Files
portfolio/.claude/skills/d3-visualization/scripts/tooltip_handler.js
T

228 lines
5.6 KiB
JavaScript

/**
* 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 `
<strong>${data.name || data.id}</strong><br/>
${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) => `
<strong>${d.ticker}</strong><br/>
${d.name}<br/>
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 = [
`<div class="tooltip-header">${d.name}</div>`,
`<div class="tooltip-body">`,
` <div>Ticker: ${d.ticker}</div>`,
` <div>Sector: ${d.sector}</div>`,
];
if (d.marketCap) {
parts.push(` <div>Market Cap: ${formatNumber(d.marketCap)}</div>`);
}
parts.push(`</div>`);
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;
}