D3.js中.on("click")事件仅对每个圆点生效一次的问题求助
问题描述
我是D3.js和JavaScript新手,对join()方法与事件处理存在理解误区。开发了一个基于两级绩效评级("satisfactory"和"exemplary")的薪资调整演示程序:D3图表用圆点代表部门员工,X轴为当前薪资,Y轴为年度加薪额。点击圆点应将员工在两个分组间切换,调整其加薪额并保持总加薪额不变。
问题现象:首次点击每个圆点切换分组时一切正常,图表与数据均按预期更新,但切换后的圆点无法再次点击切换回原分组,未点击过的圆点仍可正常响应点击。
完整代码
JavaScript代码(mwe.js)
// Sort of works, 18 Aug 2022, but each dot can be clicked only once. function setSal4() { var svg = d3.select( "svg" ); var pxX = +svg.attr( "width" ); var pxY = svg.attr( "height" )/2; // using two half-height plots, with different y-offsets; each one gets half the square var paddingfactor = 0.2; // shrink plot by some factor from all edges var minsatpct = 2.2/100 ; // hardcode raise limits, in percent var maxsatpct = 2.9/100; var minexpct = 3.2/100; var maxexpct = 4.0/100 ; // used only to set graph y-axis limit; not used in calculations // build maps for the two groups, with // names as keys, and 2-element arrays containing current // salary and dollar raise as values (raise set to 0 at start) // all names must be unique // use ">0" test to handle inputs with different numbers of sat and exem people // initialize values that should be stable during session, to be constructed during map building var minsal=Number.MAX_VALUE; var maxsal=0; var totalsal = 0; // build maps containing current salaries for each group var exemplaries = new Map(); var satisfactories = new Map(); exemplaries.set("alice",[+68000,+0]); exemplaries.set("bob",[+64000,+0]); exemplaries.set("carol",[+72000,+0]); satisfactories.set("dan",[+74000,+0]); satisfactories.set("ellen",[+66000,+0]); satisfactories.set("frank",[+62000,+0]); // find minimum, maximum, and total salaries for (const person of exemplaries) { const sal = person[1][0]; totalsal += sal; minsal = Math.min(minsal, sal); maxsal = Math.max(maxsal, sal); } for (const person of satisfactories) { const sal = person[1][0]; totalsal += sal; minsal = Math.min(minsal, sal); maxsal = Math.max(maxsal, sal); } var raisepool = totalsal*maxsatpct; // total amount available for raises console.log("min, max, total, pool = ",minsal, maxsal, totalsal, raisepool); // establish raises according to policy setRaises("flatpct"); reportGroups(); // set up scaling objects to map salaries and raises onto screen area var scX = d3.scaleLinear().domain([minsal,maxsal]).range([paddingfactor*pxX, (1.0-paddingfactor)*pxX] ).nice(); var scYdol = d3.scaleLinear().domain([minsatpct*minsal,maxexpct*maxsal]).range([(1.0-paddingfactor)*pxY+pxY, paddingfactor *pxY+pxY] ).nice(); // make groups to contain data points for satisfactory and exemplary groups var g1 = svg.append( "g" ); var g2 = svg.append( "g" ); redrawData(); // joins data to circles and draws all four groups detectAction("circle"); // install event handler // end of main program; function definitions follow // event handler for clicking on a dot // click on circle to switch person between Satisfactory and Exemplary, adjust remaining Sat salaries, redraw function detectAction( selector ) { var svg = d3.selectAll( selector ) .on( "click", function(event,d) { console.log("clicked on ", d[0], "salary ", d[1][0]); var key = d[0]; swapGroups(key); setRaises("flatpct"); redrawData(); reportGroups(); } ) } // join and draw circles corresponding to all people in a single dataset function drawData( g, dataset, keyfunc, xaccessor, yaccessor, color) { g.selectAll( "circle" ) .data(dataset,keyfunc) .join('circle') .attr( "r", 5 ) .attr( "cx", xaccessor) .attr( "cy", yaccessor ); g.selectAll( "circle" ).attr( "fill", color ); } // call DrawData function for all datasets function redrawData() { drawData( g1, exemplaries, d => d[0], d => scX(d[1][0]), d => scYdol(d[1][1]), "green" ); drawData( g2, satisfactories, d => d[0], d => scX(d[1][0]), d => scYdol(d[1][1]) , "blue"); } // given map of people with salaries, and constant percent raise, assign raises to each function setFlatPercentRaises(cohort,pct) { for (const person of cohort) { person[1][1] = pct*person[1][0]; } } function setRaises (type) { // set raises for everyone, using a specific model set by 'type' argument if (type == "minpct") { // will not use all of raise pool setFlatPercentRaises(exemplaries, minexpct); setFlatPercentRaises(satisfactories, minsatpct); } else if (type == "flatpct") { // minimum raises to exem, best flat % available to sat setFlatPercentRaises(exemplaries, minexpct); [exemtot, sattot] = gettotals(exemplaries, satisfactories); satpct = (raisepool-exemtot*minexpct)/sattot; // find remaining amount for sat; should check to see if there's enough setFlatPercentRaises(satisfactories, satpct); } } function gettotals(map1, map2) { var tot1 = 0; for (const d of map1) { tot1+= d[1][0]; } var tot2=0; for (const d of map2) { tot2+= d[1][0]; } return [tot1,tot2]; } function swapGroups(key) { console.log("key is ", key); console.log("lengths (exem, sat) = ", exemplaries.size, satisfactories.size); if (exemplaries.has(key)) { satisfactories.set(key,exemplaries.get(key)); // add entry to other group exemplaries.delete(key); // remove from this group console.log("lengths (exem, sat) = ", exemplaries.size, satisfactories.size) } else if (satisfactories.has(key)) { exemplaries.set(key,satisfactories.get(key)); satisfactories.delete(key); console.log("lengths (exem, sat) = ", exemplaries.size, satisfactories.size); } } function reportGroups() { for (const d of exemplaries) {console.log("E: ", d[0], d[1][0], d[1][1])}; for (const d of satisfactories) {console.log("S: ",d[0], d[1][0], d[1][1])}; } }
HTML代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="d3.js"></script> <script src="mwe.js"></script> </head> <body onload="setSal4()"> <svg id="demo1" width="600" height="600" style="background: lightgrey" /> </body> </html>
解决方案
问题根源
你仅在初始化时调用一次detectAction("circle")绑定点击事件,但每次调用redrawData()时,join()会创建新的circle元素(员工切换分组时,原组的circle被移除,新组添加新circle),这些新元素未绑定点击事件,导致无法响应后续点击。
另外,drawData中单独调用g.selectAll("circle").attr("fill", color)会冗余覆盖所有circle颜色,可优化到链式调用中。
修复步骤
- 将事件绑定整合到
drawData函数:每次join()完成后,直接为元素绑定点击事件,确保所有circle都有监听。 - 优化
drawData链式调用:把颜色设置并入join()后续操作,避免额外选择器调用。 - 移除全局
detectAction调用:事件绑定已在drawData中完成,无需单独调用。
修改后的关键代码
调整drawData函数
function drawData( g, dataset, keyfunc, xaccessor, yaccessor, color) { g.selectAll( "circle" ) .data(dataset,keyfunc) .join('circle') .attr( "r", 5 ) .attr( "cx", xaccessor) .attr( "cy", yaccessor ) .attr("fill", color) .on("click", function(event,d) { console.log("clicked on ", d[0], "salary ", d[1][0]); var key = d[0]; swapGroups(key); setRaises("flatpct"); redrawData(); reportGroups(); }); }
删除初始化时的事件绑定调用
移除这一行代码:
detectAction("circle"); // install event handler
(可选)删除原detectAction函数
该函数已无作用,可直接删除。
修复原理
每次redrawData()执行时,join()会处理数据与元素的匹配:移除离开组的员工对应的circle,添加新进入组的员工对应的circle。通过在join()后直接绑定点击事件,新创建的circle自动获得监听,保留的circle的事件也不会丢失,从而实现所有circle均可反复响应点击。
内容的提问来源于stack exchange,提问作者George McBane
相关产品推荐
相关产品推荐

