d3-interactive-charts · git:20260424.d50cf5a · 2026-04-24 · sha256 9003b713e312cf46

d3-interactive-charts git:20260424.d50cf5aA

Immutable. This exact content never changes and is served at /api/v1/blob/9003b713e312cf46.

---
name: d3-interactive-charts
description: D3.js techniques for tooltips, hover effects, click handlers, and cross-filtering between visualizations
---

# D3.js Interactive Charts

## Overview
Making D3 charts interactive with tooltips, highlighting, and coordinated interactions between multiple charts.

## 1. Tooltips

### HTML Tooltip (Best for Hover)
```javascript
// Create tooltip div
const tooltip = d3.select("body")
  .append("div")
  .attr("class", "tooltip")
  .style("position", "absolute")
  .style("background", "rgba(0,0,0,0.8)")
  .style("color", "white")
  .style("padding", "8px 12px")
  .style("border-radius", "4px")
  .style("pointer-events", "none")
  .style("opacity", 0)
  .style("z-index", 1000);

// Add mouseover handlers
circles
  .on("mouseover", (event, d) => {
    tooltip
      .style("opacity", 1)
      .html(`<strong>${d.ticker}</strong><br/>${d.name}<br/>${d.sector}`)
      .style("left", event.pageX + 10 + "px")
      .style("top", event.pageY + 10 + "px");
  })
  .on("mousemove", (event) => {
    tooltip
      .style("left", event.pageX + 10 + "px")
      .style("top", event.pageY + 10 + "px");
  })
  .on("mouseout", () => {
    tooltip.style("opacity", 0);
  });
```

### CSS Styling for Tooltip
```css
.tooltip {
  position: absolute;
  padding: 8px 12px;
  background: rgba(0, 0, 0, 0.85);
  color: white;
  border-radius: 4px;
  font-size: 12px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
  z-index: 1000;
  white-space: nowrap;
}
```

## 2. Hover Effects

### Highlight on Hover
```javascript
circles
  .on("mouseover", (event, d) => {
    // Highlight the hovered circle
    d3.select(event.currentTarget)
      .transition()
      .duration(200)
      .attr("opacity", 1)
      .attr("stroke", "black")
      .attr("stroke-width", 2);

    // Fade other circles
    circles.filter(nd => nd.id !== d.id)
      .transition()
      .duration(200)
      .attr("opacity", 0.3);
  })
  .on("mouseout", () => {
    // Restore all
    circles
      .transition()
      .duration(200)
      .attr("opacity", 1)
      .attr("stroke", "none");
  });
```

## 3. Click Handlers & Selection

### Click to Select
```javascript
let selectedId = null;

circles
  .on("click", (event, d) => {
    selectedId = (selectedId === d.id) ? null : d.id;

    circles
      .attr("opacity", nd => selectedId === null || nd.id === selectedId ? 1 : 0.3)
      .attr("stroke", nd => nd.id === selectedId ? "black" : "none")
      .attr("stroke-width", nd => nd.id === selectedId ? 2 : 0);

    // Trigger update in other visualizations
    updateTable(selectedId);
  });
```

## 4. Cross-Filtering Between Charts

### Pattern: Coordinating Multiple Visualizations

```javascript
// Define shared selection state
let selectedTicker = null;

// Update when bubble is clicked
function selectBubble(ticker) {
  selectedTicker = ticker;

  // Highlight bubble
  circles.attr("opacity", d => d.ticker === ticker ? 1 : 0.3);

  // Highlight table row
  tableRows.attr("class", d => d.ticker === ticker ? "selected" : "");
}

// Update when table row is clicked
function selectTableRow(ticker) {
  selectedTicker = ticker;

  // Highlight table row
  tableRows.attr("class", d => d.ticker === ticker ? "selected" : "");

  // Highlight bubble
  circles.attr("opacity", d => d.ticker === ticker ? 1 : 0.3);
}

// Setup handlers
circles.on("click", (event, d) => {
  selectBubble(d.ticker);
});

tableRows.on("click", (event, d) => {
  selectTableRow(d.ticker);
});
```

### CSS for Selection Highlighting
```css
.selected {
  background-color: #FFC700 !important;
  font-weight: bold;
}

tr.selected {
  background-color: #FFC700;
}
```

## 5. Dynamic Updates (Filtering/Sorting)

### Update visualization when data changes
```javascript
function updateChart(newData) {
  // Update scales
  colorScale.domain(Array.from(new Set(newData.map(d => d.sector))));

  // Rebind data
  circles = circles.data(newData, d => d.id);

  // Enter + Update + Exit pattern
  circles
    .join(
      enter => enter.append("circle")
        .attr("r", d => rScale(d.value))
        .attr("fill", d => colorScale(d.sector)),
      update => update,
      exit => exit.remove()
    )
    .transition()
    .duration(500)
    .attr("cx", d => xScale(d.x))
    .attr("cy", d => yScale(d.y));
}
```

## 6. Cursor & Visual Feedback

### Indicate Interactivity
```css
circle {
  cursor: pointer;
  transition: opacity 0.2s, stroke 0.2s;
}

circle:hover {
  cursor: pointer;
}

tr:hover {
  background-color: #f5f5f5;
  cursor: pointer;
}
```

## 7. Event Delegation

### Handle events on parent, not children
```javascript
// Less efficient - attaches handler to many elements
circles.on("click", handler);

// More efficient - single handler on parent
g.on("click", (event, d) => {
  if (event.target.tagName === "circle") {
    handleCircleClick(d);
  }
});
```

## Best Practices
- Use `event.currentTarget` vs `this` for modern D3
- Debounce mousemove events for performance
- Clean up event handlers when removing visualizations
- Use CSS transitions alongside D3 transitions for smooth UX
- Provide visual feedback for all interactive elements