/**
* 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 = [
`