You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 15:18:02