如何检测 D3 中条形图上指针下方最近的矩形?

How to detect closest rect below a pointer on a bar chart in D3?

我使用 D3 创建了一个条形图,但我希望当我的指针位于矩形上方时检测该矩形并更改其颜色,例如:

因为我的指针在右起第三个矩形的上方,所以会选择那个。有办法实现吗?

这是我当前的代码:

const width =  620;
const height = 280;

const svg = d3.selectAll(".canvas")
    .append('svg')
    .style('display', 'block')
    .attr('viewBox', `0 0 ${width} ${height}`)
    .attr('preserveAspectRatio','xMinYMin')


const margin = {top:50, bottom:50, left: 50, right: 50}
const graphWidth = width - margin.right - margin.right
const graphHeight = height - margin.bottom - margin.top

const graph = svg.append('g')
    .attr('width', graphWidth)
    .attr('height', graphHeight)
    .attr('transform', `translate(${margin.left},${margin.top})`)

const xAxisGroup = graph.append('g')
    .attr('transform', `translate(0, ${graphHeight})`)

const yAxisGroup = graph.append('g')

d3.csv('./SixContinentFirst.csv').then(data => {
    africaData = data.map(obj => {
        return {infected: +(obj.Africa || '0'), date: obj.Dates}
    })
    console.log(africaData)

    const y = d3.scaleLinear()
        .domain([0, d3.max(africaData, data => data.infected)])
        .range([graphHeight,0])

    const x = d3.scaleBand()
        .domain(africaData.map(item => item.date))
        .range([0,graphWidth])
        .paddingInner(0.2)
        .paddingOuter(0.2)

    const rects = graph.selectAll('rect')
        .data(africaData)


    rects.enter()
        .append('rect')
        .attr('width', x.bandwidth)
        .attr('height', d => graphHeight - y(d.infected))
        .attr('fill', 'orange')
        .attr('x', (d) => x(d.date))
        .attr('y', d => y(d.infected))
        .attr('rx', 8)
        .attr('ry', 8)
        // .on('mousemove', (d, i) => {
        //         console.log("Hover")
        //     })

    const xAxis = d3.axisBottom(x)
        .tickFormat((d,i) => i % 6 === 0 ? d : '')


    let formatter = Intl.NumberFormat('en', { notation: 'compact' });

    const yAxis = d3.axisRight(y)
        .ticks(3)
        .tickFormat(d => formatter.format(+d))


    xAxisGroup.call(xAxis)

    yAxisGroup.call(yAxis)
        .attr('transform', `translate(${graphWidth}, 0)`)
        .call(g => g.select('.domain').remove())
        .call(g => g.selectAll('line').remove())
        .selectAll('text')
        .attr("font-size", "10")

    xAxisGroup
        .call(g => g.select('.domain').remove())
        .call(g => g.selectAll('line').remove())
        .selectAll('text')
        .attr("font-size", "10")
    
})

当我将“mousemove”事件添加到“rects”元素时,它只会检测到我直接悬停在 rect 上的时间,但不会检测到我在其上方的时间。

一种方法是绘制两个 rect - 一个填满整个 graphHeight 并且没有填充(确保将 pointer-events 设置为 all)然后画出你原来的矩形。这个 'background' rect 然后可以响应鼠标事件,你可以 select 'foreground' rect 并更改 属性 等。我用过 ids基于索引方便selection.

为了完成这项工作,你需要为每对 rects(前景和背景)包含一个,然后你可以根据是否鼠标在矩形上或其上:

  const rects = graph.selectAll('rect')
    .data(africaData)
    .enter()
    .append("g")
    .attr("class", "rect-container");

  // background rect
  rects.append('rect')
    // set properties and events
    .on('mouseover', function(d, i) {
      // handle event
    })
    .on('mouseout', function(d, i) {
      // handle event
    })
    
  // foreground rect
  rects.append('rect')
    // set properties and events
    .on('mouseover', function(d, i) {
      // handle event
    })
    .on('mouseout', function(d, i) {
      // handle event
    })

根据您的原始代码见下文,但有一些虚拟数据:

const width =  620;
const height = 280;

const svg = d3.selectAll(".canvas")
  .append('svg')
  .style('display', 'block')
  .attr('viewBox', `0 0 ${width} ${height}`)
  .attr('preserveAspectRatio','xMinYMin');

const margin = {top:20, bottom:20, left: 20, right: 20}
const graphWidth = width - margin.right - margin.right;
const graphHeight = height - margin.bottom - margin.top;

const graph = svg.append('g')
  .attr('width', graphWidth)
  .attr('height', graphHeight)
  .attr('transform', `translate(${margin.left},${margin.top})`);

const xAxisGroup = graph.append('g')
  .attr('transform', `translate(0, ${graphHeight})`);

const yAxisGroup = graph.append('g');

// data for this example
const data = [
  { Africa: 7, Dates: "Jan 2022" },
  { Africa: 1, Dates: "Feb 2022" },
  { Africa: 0, Dates: "Mar 2022" },
  { Africa: 11, Dates: "Apr 2022" },
  { Africa: 7, Dates: "May 2022" },
  { Africa: 7, Dates: "Jun 2022" },
  { Africa: 16, Dates: "Jul 2022" },
  { Africa: 2, Dates: "Aug 2022" },
  { Africa: 8, Dates: "Sep 2022" },
  { Africa: 3, Dates: "Oct 2022" },
  { Africa: 15, Dates: "Nov 2022" },
  { Africa: 12, Dates: "Dec 2022" },
];

// render viz 
render(data);

//d3.csv('./SixContinentFirst.csv').then(data => {
function render(data) {

  africaData = data.map(obj => {
    return {infected: +(obj.Africa || '0'), date: obj.Dates}
  });
  //console.log(africaData);

  const y = d3.scaleLinear()
    .domain([0, d3.max(africaData, data => data.infected)])
    .range([graphHeight,0]);

  const x = d3.scaleBand()
    .domain(africaData.map(item => item.date))
    .range([0,graphWidth])
    .paddingInner(0.2)
    .paddingOuter(0.2);

  const rects = graph.selectAll('rect')
    .data(africaData)
    .enter()
    .append("g")
    .attr("class", "rect-container");

  // background rect
  rects.append('rect')
    .attr('width', x.bandwidth)
    .attr('height', d => graphHeight)
    .attr('fill', 'none')    
    .attr('x', (d) => x(d.date))
    .attr('y', d => 0)
    .attr('pointer-events', 'all')
    .on('mouseover', function(d, i) {
      d3.select(`#bar_${i}`).attr('fill', 'red')
    })
    .on('mouseout', function(d, i) {
      d3.select(`#bar_${i}`).attr('fill', 'orange')
    })
    
  // foreground rect
  rects.append('rect')
    .attr('width', x.bandwidth)
    .attr('height', d => graphHeight - y(d.infected))
    .attr('fill', 'orange')
    .attr('id', (d, i) => `bar_${i}`)    
    .attr('class', "foreground-rect")    
    .attr('x', (d) => x(d.date))
    .attr('y', d => y(d.infected))
    .attr('rx', 8)
    .attr('ry', 8)
    .attr('pointer-events', 'all')
    .on('mouseover', function(d, i) {
      d3.select(`#bar_${i}`).attr('fill', 'green')
    })
    .on('mouseout', function(d, i) {
      d3.select(`#bar_${i}`).attr('fill', 'orange')
    })

  const xAxis = d3.axisBottom(x)
    .tickFormat((d,i) => i % 6 === 0 ? d : '');

  let formatter = Intl.NumberFormat('en', { notation: 'compact' });

  const yAxis = d3.axisRight(y)
    .ticks(3)
    .tickFormat(d => formatter.format(+d));

  xAxisGroup.call(xAxis);

  yAxisGroup.call(yAxis)
    .attr('transform', `translate(${graphWidth}, 0)`)
    .call(g => g.select('.domain').remove())
    .call(g => g.selectAll('line').remove())
    .selectAll('text')
    .attr("font-size", "10");

  xAxisGroup
    .call(g => g.select('.domain').remove())
    .call(g => g.selectAll('line').remove())
    .selectAll('text')
    .attr("font-size", "10");

}
//)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="canvas"></div>