chore: auto-commit before merge (loop primary)

This commit is contained in:
2026-02-16 14:36:25 +00:00
parent 9276955fa8
commit aca57714e4
23 changed files with 1859 additions and 513 deletions
@@ -0,0 +1,129 @@
/**
* Complete Bubble Chart Example with Force Simulation
*
* This example shows how to create a clustered bubble chart with:
* - Force-directed layout
* - Collision detection
* - Color coding by category
* - Size scaling by value
*/
// Configuration
const width = 800;
const height = 600;
const margin = {top: 20, right: 20, bottom: 20, left: 20};
// Create SVG
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
// Load and process data
d3.csv('/data/stocks.csv').then(data => {
// Parse numbers
data.forEach(d => {
d.value = d.marketCap ? +d.marketCap : null;
d.ticker = d.ticker;
d.sector = d.sector;
});
createBubbleChart(data);
});
function createBubbleChart(data) {
// Setup scales
const radiusScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.value || 0)])
.range([5, 50]);
const colorScale = d3.scaleOrdinal()
.domain([...new Set(data.map(d => d.sector))])
.range(d3.schemeCategory10);
// Calculate radius for each data point
data.forEach(d => {
if (d.value === null || isNaN(d.value)) {
d.radius = 10; // Uniform size for missing data
d.hasValue = false;
} else {
d.radius = radiusScale(d.value);
d.hasValue = true;
}
});
// Create force simulation
const simulation = d3.forceSimulation(data)
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(d => d.radius + 2))
.force('charge', d3.forceManyBody().strength(-30));
// Optional: Cluster by sector
const sectors = [...new Set(data.map(d => d.sector))];
const sectorPositions = {};
sectors.forEach((sector, i) => {
const angle = (i / sectors.length) * 2 * Math.PI;
sectorPositions[sector] = {
x: width / 2 + Math.cos(angle) * 150,
y: height / 2 + Math.sin(angle) * 150
};
});
simulation
.force('x', d3.forceX(d => sectorPositions[d.sector].x).strength(0.5))
.force('y', d3.forceY(d => sectorPositions[d.sector].y).strength(0.5));
// Create bubbles
const bubbles = svg.selectAll('circle')
.data(data)
.join('circle')
.attr('class', 'bubble')
.attr('r', d => d.radius)
.attr('fill', d => colorScale(d.sector))
.attr('opacity', 0.7)
.on('mouseover', function(event, d) {
// Only show tooltip if has complete data
if (d.sector !== 'ETF') {
showTooltip(event, d);
}
})
.on('mouseout', hideTooltip)
.on('click', function(event, d) {
selectBubble(d.ticker);
});
// Update positions on each tick
simulation.on('tick', () => {
bubbles
.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
}
function showTooltip(event, d) {
const tooltip = d3.select('#tooltip');
tooltip
.classed('visible', true)
.html(`
<strong>${d.ticker}</strong><br/>
${d.name || 'N/A'}<br/>
Sector: ${d.sector}
`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px');
}
function hideTooltip() {
d3.select('#tooltip').classed('visible', false);
}
function selectBubble(ticker) {
// Highlight selected bubble
d3.selectAll('.bubble')
.classed('selected', d => d.ticker === ticker);
// Trigger table highlight if exists
if (typeof highlightTableRow === 'function') {
highlightTableRow(ticker);
}
}
@@ -0,0 +1,64 @@
/**
* Tooltip Implementation Checker
*
* This script helps verify tooltip implementation follows best practices:
* 1. Tooltips use CSS class for visibility
* 2. Tooltips are conditionally displayed
* 3. Tooltip content matches data
*/
// Example: Check if tooltip element exists
function checkTooltipSetup() {
const tooltip = document.getElementById('tooltip');
if (!tooltip) {
console.error('❌ Tooltip element not found');
return false;
}
console.log('✓ Tooltip element exists');
// Check CSS classes
const classes = window.getComputedStyle(tooltip).cssText;
console.log('Tooltip computed styles:', classes);
return true;
}
// Example: Verify tooltip has .visible class mechanism
function checkTooltipVisibility() {
const tooltip = document.getElementById('tooltip');
const hasVisibleClass = tooltip.classList.contains('visible');
console.log('Tooltip has .visible class:', hasVisibleClass);
console.log('Tooltip opacity:', window.getComputedStyle(tooltip).opacity);
return true;
}
// Example: Test tooltip content
function testTooltipContent(sampleData) {
const tooltip = document.getElementById('tooltip');
// Simulate showing tooltip
tooltip.classList.add('visible');
tooltip.innerHTML = `
<strong>${sampleData.ticker}</strong><br/>
${sampleData.name}<br/>
Sector: ${sampleData.sector}
`;
console.log('Tooltip content:', tooltip.innerHTML);
// Clean up
setTimeout(() => {
tooltip.classList.remove('visible');
}, 1000);
}
// Run checks
if (typeof document !== 'undefined') {
console.log('=== Tooltip Implementation Check ===');
checkTooltipSetup();
checkTooltipVisibility();
}
@@ -0,0 +1,186 @@
/**
* Interactive Data Table Example
*
* Creates a sortable, filterable data table that links with charts.
* Features:
* - Two-way highlighting (click table row -> highlight chart element)
* - Formatted numbers
* - Click to sort columns
*/
function createInteractiveTable(data) {
const container = d3.select('#table-container');
// Create table structure
const table = container.append('table')
.attr('id', 'data-table');
// Define columns
const columns = [
{key: 'ticker', label: 'Ticker', format: d => d},
{key: 'name', label: 'Company Name', format: d => d},
{key: 'sector', label: 'Sector', format: d => d},
{key: 'marketCap', label: 'Market Cap', format: formatNumber}
];
// Create header
const thead = table.append('thead');
const headerRow = thead.append('tr');
headerRow.selectAll('th')
.data(columns)
.join('th')
.text(d => d.label)
.on('click', function(event, col) {
sortTable(data, col.key);
})
.style('cursor', 'pointer');
// Create body
const tbody = table.append('tbody');
function renderTable(data) {
const rows = tbody.selectAll('tr')
.data(data, d => d.ticker) // Key function for stable updates
.join('tr')
.on('click', function(event, d) {
selectRow(d.ticker);
});
rows.selectAll('td')
.data(d => columns.map(col => ({
value: d[col.key],
format: col.format
})))
.join('td')
.text(d => d.format(d.value));
}
renderTable(data);
// Sorting function
let sortAscending = true;
let sortKey = null;
function sortTable(data, key) {
if (sortKey === key) {
sortAscending = !sortAscending;
} else {
sortKey = key;
sortAscending = true;
}
data.sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
if (typeof aVal === 'string') {
return sortAscending ?
aVal.localeCompare(bVal) :
bVal.localeCompare(aVal);
} else {
return sortAscending ?
(aVal || 0) - (bVal || 0) :
(bVal || 0) - (aVal || 0);
}
});
renderTable(data);
}
// Return table API
return {
update: renderTable,
sort: sortTable
};
}
function selectRow(ticker) {
// Highlight row
d3.selectAll('#data-table tbody tr')
.classed('selected', function(d) {
return d && d.ticker === ticker;
});
// Trigger chart highlight if exists
if (typeof highlightBubble === 'function') {
highlightBubble(ticker);
}
}
function highlightTableRow(ticker) {
d3.selectAll('#data-table tbody tr')
.classed('highlighted', function(d) {
return d && d.ticker === ticker;
});
}
// Number formatting utility
function formatNumber(num) {
if (num === null || num === undefined || num === '') return '-';
const absNum = Math.abs(num);
let formatted;
if (absNum >= 1e12) {
formatted = (num / 1e12).toFixed(2) + 'T';
} else if (absNum >= 1e9) {
formatted = (num / 1e9).toFixed(2) + 'B';
} else if (absNum >= 1e6) {
formatted = (num / 1e6).toFixed(2) + 'M';
} else if (absNum >= 1e3) {
formatted = (num / 1e3).toFixed(2) + 'K';
} else {
formatted = num.toFixed(2);
}
return num < 0 ? '-' + formatted : formatted;
}
// CSS for table styling
const tableStyles = `
<style>
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th {
background: #f5f5f5;
padding: 10px;
text-align: left;
border-bottom: 2px solid #ddd;
font-weight: bold;
}
th:hover {
background: #e0e0e0;
}
td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
tr:hover {
background: #f9f9f9;
}
tr.selected {
background: #e3f2fd !important;
border-left: 3px solid #2196f3;
}
tr.highlighted {
background: #fff9c4 !important;
}
</style>
`;
// Inject styles
if (typeof document !== 'undefined') {
const styleEl = document.createElement('div');
styleEl.innerHTML = tableStyles;
document.head.appendChild(styleEl.firstChild);
}
@@ -0,0 +1,227 @@
/**
* 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;
}