/** * Reusable Tooltip Handler for D3.js Visualizations * * Provides a flexible tooltip system with: * - Conditional display based on data properties * - Customizable content templates * - Automatic positioning * - CSS class-based visibility */ class TooltipHandler { constructor(options = {}) { this.selector = options.selector || '#tooltip'; this.offsetX = options.offsetX || 10; this.offsetY = options.offsetY || -10; this.shouldShow = options.shouldShow || (() => true); this.formatContent = options.formatContent || this.defaultFormat; this.tooltip = d3.select(this.selector); // Create tooltip if it doesn't exist if (this.tooltip.empty()) { this.tooltip = d3.select('body') .append('div') .attr('id', this.selector.replace('#', '')) .attr('class', 'tooltip'); } } /** * Show tooltip for a data point * @param {Event} event - Mouse event * @param {Object} data - Data point */ show(event, data) { // Check if tooltip should be shown for this data if (!this.shouldShow(data)) { return; } const content = this.formatContent(data); this.tooltip .classed('visible', true) .html(content) .style('left', (event.pageX + this.offsetX) + 'px') .style('top', (event.pageY + this.offsetY) + 'px'); } /** * Hide tooltip */ hide() { this.tooltip.classed('visible', false); } /** * Move tooltip to follow mouse * @param {Event} event - Mouse event */ move(event) { if (this.tooltip.classed('visible')) { this.tooltip .style('left', (event.pageX + this.offsetX) + 'px') .style('top', (event.pageY + this.offsetY) + 'px'); } } /** * Default content formatter * @param {Object} data - Data point * @returns {string} HTML content */ defaultFormat(data) { return ` ${data.name || data.id}
${data.value ? 'Value: ' + data.value : ''} `; } /** * Attach tooltip handlers to D3 selection * @param {d3.Selection} selection - D3 selection */ attach(selection) { const self = this; selection .on('mouseover', function(event, d) { self.show(event, d); }) .on('mousemove', function(event) { self.move(event); }) .on('mouseout', function() { self.hide(); }); return selection; } } // Example usage patterns /** * Example 1: Basic tooltip */ function example1() { const tooltip = new TooltipHandler(); d3.selectAll('circle') .on('mouseover', (event, d) => tooltip.show(event, d)) .on('mouseout', () => tooltip.hide()); } /** * Example 2: Conditional tooltip (exclude specific categories) */ function example2() { const tooltip = new TooltipHandler({ shouldShow: (d) => d.category !== 'ETF', // Don't show for ETFs formatContent: (d) => ` ${d.ticker}
${d.name}
Sector: ${d.sector} ` }); tooltip.attach(d3.selectAll('circle')); } /** * Example 3: Rich formatted tooltip */ function example3() { const tooltip = new TooltipHandler({ shouldShow: (d) => d.hasCompleteData, formatContent: (d) => { const parts = [ `
${d.name}
`, `
`, `
Ticker: ${d.ticker}
`, `
Sector: ${d.sector}
`, ]; if (d.marketCap) { parts.push(`
Market Cap: ${formatNumber(d.marketCap)}
`); } parts.push(`
`); return parts.join(''); } }); tooltip.attach(d3.selectAll('.bubble')); } /** * Example 4: Multiple tooltips with different styles */ function example4() { // Tooltip for bubbles const bubbleTooltip = new TooltipHandler({ selector: '#bubble-tooltip', shouldShow: (d) => d.type !== 'excluded' }); // Tooltip for table cells const tableTooltip = new TooltipHandler({ selector: '#table-tooltip', formatContent: (d) => `Details: ${d.description}` }); bubbleTooltip.attach(d3.selectAll('.bubble')); tableTooltip.attach(d3.selectAll('.info-cell')); } // Helper function for number formatting function formatNumber(num) { if (!num) return '-'; if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T'; if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B'; if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'; if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K'; return num.toFixed(2); } // Required CSS (add to your stylesheet) const requiredCSS = ` .tooltip { position: absolute; padding: 10px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 4px; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 1000; font-size: 14px; line-height: 1.4; } .tooltip.visible { opacity: 1; } .tooltip-header { font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid rgba(255,255,255,0.3); padding-bottom: 3px; } .tooltip-body { font-size: 12px; } .tooltip-body div { margin: 2px 0; } `; // Export for use in modules if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipHandler; }