如何在D3.js中基于数据字段条件动态设置SVG路径的属性名
问题原因
d3的.attr()方法不支持直接将动态属性名作为第一个函数参数传入,你写的.attr(d => chkArrowDir(d), 'url(#arrowhead)')会被d3识别为把函数本身作为属性名设置,自然会抛出脚本错误。
解决方案
你可以用.attr()传入返回属性对象的函数的写法,实现动态属性名设置,同时处理差值反转的逻辑:
修正箭头属性设置代码
原来的错误代码:
.attr(d => chkArrowDir(d), 'url(#arrowhead)')
替换为:
.attr(d => ({ [chkArrowDir(d)]: 'url(#arrowhead)' }))
差值百分比反转逻辑
如果需要同步根据isReverse反转差值计算结果,把原来的百分比计算代码修改为:
.text((d, i) => { if (d.Target == -1) return '' const originDiff = Math.round((barData[d.Target].Value / d.Value * 100) - 100) const finalDiff = d.isReverse === 'true' ? -originDiff : originDiff return `${finalDiff > 0 ? '+' : ''}${finalDiff}%` })
完整可运行修正代码
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> <div id="graph"></div>
CSS
#graph { width: 600px; height: 400px; } text { font-size: 12px; font-family: "Ubuntu"; } .toolTip { position: absolute; display: none; min-width: 80px; height: auto; background: none repeat scroll 0 0 #ffffff; border: 1px solid #6F257F; padding: 5px; text-align: left; }
JavaScript
var barData = [{ "Time": "Bar1", "Value": 5388, "Target": 1, "isReverse": "true" }, { "Time": "Bar2", "Value": 6453, "Target": 3, "isReverse": "false" }, { "Time": "Bar3", "Value": 3345, "Target": -1, "isReverse": "false" }, { "Time": "Bar4", "Value": 5345, "Target": 0, "isReverse": "false" }]; const container = d3.select('#graph'); const divWidth = parseInt(container.style('width')); const divHeight = parseInt(container.style('height')); const margin = {top: 30, right: 50, bottom: 50, left: 50}; const width = divWidth - margin.left - margin.right; const height = divHeight - margin.top - margin.bottom; const svg = container.append("svg") .attr("width", divWidth) .attr("height", divHeight); const svgG = svg.append("g") .attr("transform", `translate(${margin.left},${margin.top})`); var tooltip = d3.select("body").append("div").attr("class", "toolTip"); const defs = svg.append("defs"); const marker = defs.append("marker") .attr("id","arrowhead") .attr("markerWidth","10") .attr("markerHeight","7") .attr("refX","0") .attr("refY","3.5") .attr("orient","auto") const polygon = marker.append("polygon") .attr("fill","gray") .attr("points","0 0, 10 3.5, 0 7") const xScale = d3.scaleBand() .domain(barData.map(d => d.Time)) .range([0, width+margin.right]); const xAxis = d3.axisBottom(xScale); svgG.append('g') .attr("transform", `translate(0,${height})`) .call(xAxis); const yAxisMax = barData.reduce((max, item) => Math.max(max, item.Value), 0) * 1.5; const yScale = d3.scaleLinear() .domain([0, yAxisMax]) .range([height, 0]); const yAxis = d3.axisLeft(yScale).ticks(4); svgG.append('g') .call(yAxis); const bars = svgG.selectAll('g.bar') .data(barData) .enter() .append('g') .classed('bar', true) .attr('transform', d => `translate(${xScale(d.Time) + xScale.bandwidth() / 2}, 0)`); bars.append('rect') .attr('x', -20) .attr('width', 40) .attr('y', d => yScale(d.Value)) .attr('height', d => height - yScale(d.Value) ) .attr('fill', 'blue') .on("mousemove", onMouseOver) .on("mouseout", onMouseOut); function onMouseOver(d,i) { tooltip .style("left", d3.event.pageX - 50 + "px") .style("top", d3.event.pageY - 70 + "px") .style("display", "inline-block") .html("年份: " + (d.Time) + "<br>" + "数值: " + (d.Value)); d3.select(this).attr('fill', "#eec42d"); } function onMouseOut(d,i) { tooltip.style("display", "none"); d3.select(this).attr('fill', "blue"); } bars.append('text') .text(d => d.Value) .attr('text-anchor', 'middle') .attr('y', d => yScale(d.Value)) .attr('dy', -5); const topPosition = i => yScale(9000) + i * 15; const pathBetweenBars = (d, i) => { if(d.Target != -1) { const delta = d.Target - i; const targetValue = barData[d.Target].Value; const targetX = delta * xScale.bandwidth() - 5; const sourceY = yScale(d.Value); const targetY = yScale(targetValue); const topY = topPosition(i); return `M 5,${sourceY - 20} V ${topY} H ${targetX} V ${targetY - 25}`; } else { return `M 5,${-100} V ${-100} H ${-1000} V ${-0}`; } }; const LABEL_WIDTH = 50; const midPosition = (d, i) => { if(d.Target != -1) { const delta = d.Target - i; return delta * xScale.bandwidth() / 2; } else { return -1000; } } function chkArrowDir(d) { return d.isReverse == "false" ? 'marker-end' : 'marker-start'; } bars.filter(d => d.Target != null) .append('path') .attr('d', (d, i) => pathBetweenBars(d, i)) .style('stroke', 'gray') .style('fill', 'none') .attr(d => ({ [chkArrowDir(d)]: 'url(#arrowhead)' })) bars.filter((d) => d.Target != null) .append('rect') .attr('x', (d, i) => midPosition(d, i) - LABEL_WIDTH / 2) .attr('y', (d, i) => topPosition(i) - 10) .attr('width', LABEL_WIDTH) .attr('height', 20) .attr('rx', 10) .style('fill', 'white') .style('stroke', 'gray'); bars.filter((d, i) => d.Target != null) .append('text') .text((d, i) => { if (d.Target == -1) return '' const originDiff = Math.round((barData[d.Target].Value / d.Value * 100) - 100) const finalDiff = d.isReverse === 'true' ? -originDiff : originDiff return `${finalDiff > 0 ? '+' : ''}${finalDiff}%` }) .attr('x', (d, i) => midPosition(d, i)) .attr('y', (d, i) => topPosition(i) + 3) .attr('text-anchor', 'middle') .style('fill', 'black');
内容的提问来源于stack exchange,提问作者Amit kumar Sah
相关产品推荐
相关产品推荐

