You need to enable JavaScript to run this app. Qlik Sense堆叠条形图:d3.js v4解决tooltip鼠标跟随错位问题
优惠活动
大模型
产品
解决方案
定价
更多

Qlik Sense堆叠条形图:d3.js v4解决tooltip鼠标跟随错位问题

Qlik Sense Cloud 自定义堆叠条形图Tooltip定位异常问题

问题基本情况

  • 开发场景:基于Qlik Sense Cloud环境,使用D3.js v4框架开发堆叠条形图自定义扩展,最终图表效果如下:
    图表效果
  • 异常表现:当悬停USA区域的蓝色堆叠段时,tooltip未正确对齐到对应鼠标位置,反而错误显示在Japan、Nordic段上方的空白区域
  • 复现条件:使用含.qvf文件的测试压缩包,在对应Qlik Sense版本中可稳定复现问题
  • 环境验证结果:同一份代码在独立JSFiddle环境中运行正常,仅在Qlik Sense容器环境下出现定位偏差,不符合预期
  • 前期排查动作:已在Qlik Sense官方社区提交求助帖,暂未得到回复

现有实现逻辑

核心Tooltip定位代码

当前实现Tooltip跟随逻辑的核心代码片段如下:

.on("mousemove", function(d) {
  var xPosition = d3.event.pageX;
  var yPosition = d3.event.pageY;

  toolTip.style("left", xPosition + 10 + "px");
  toolTip.style("top", yPosition - 25 + "px");
  toolTip.style("position", "absolute");
  toolTip.style("display", "inline-block");
  tooltip.html(...)
})

完整项目代码

绘图与交互逻辑(JavaScript)

data=[
  {dValues: "Germany", m1Values: 3323936.920000017, m2Values: 1469934.4999999946},
  {dValues: "Japan", m1Values: 11847615.030000022, m2Values: 4860290.499999991},
  {dValues: "Nordic", m1Values: 10382965.910000034, m2Values: 4289934.389999997},
  {dValues: "Spain", m1Values: 3449601.7199999965, m2Values: 1594701.149999997},
  {dValues: "UK", m1Values: 28157182.22999989, m2Values: 12590207.999999987},
  {dValues: "USA", m1Values: 47691372.99999974, m2Values: 18448120.5719999}
]

var quarters = data.map(function(d) { return d.dValues; })
var layers = d3.stack()
  .keys(["m1Values", "m2Values", "m3Values"])
  .order(d3.stackOrderNone)
  .offset(d3.stackOffsetNone)(data);
var max = d3.max(layers[layers.length-1], function(d) { return d[1]; });

// 数值格式化工具函数
var roundNumber = function roundNumber(num, noPrecision){
  if (/^[0-9.]+$/.test(num)) {
    num = !noPrecision ? parseFloat(num).toFixed(2) : Math.round(num);
    if (num >= 1000 && num < 1000000) {
      num = !noPrecision ? parseFloat(num / 1000).toFixed(2) : Math.round(num / 1000);
      if (/\.00$/.test(num)) num = num.replace(/\.00$/, '');
      num += 'K';
    } else if (num >= 1000000 && num < 1000000000) {
      num = !noPrecision ? parseFloat(num / 1000000).toFixed(2) : Math.round(num / 1000000);
      if (/\.00$/.test(num)) num = num.replace(/\.00$/, '');
      num += 'M';
    } else if (num >= 1000000000) {
      num = !noPrecision ? parseFloat(num / 1000000000).toFixed(2) : Math.round(num / 1000000000);
      if (/\.00$/.test(num)) num = num.replace(/\.00$/, '');
      num += 'T';
    }
  }
  return num;
};

// 图表尺寸与边距配置
var margin = { top: 20, right: 20, bottom: 50, left: 60 };
var width = 960 - margin.left - margin.right,
  height = 500 - margin.top - margin.bottom;

// 坐标轴比例尺配置
var x = d3.scaleBand()
  .range([0, width])
  .padding(0.3)
var y = d3.scaleLinear()
  .range([height, 0]);

color = null;
color = d3.scaleOrdinal().range(["#98abc5", "#8a89a6", "#7b6888"]);
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "dValues"; }));

// 组装堆叠条形所需数据格式
data.forEach(function(d) {
  var y0 = 0;
  d.values = color.domain().map(function(name) { 
    return {name: name, y0: y0, y1: y0 += +d[name]}; 
  });
  d.total = d.values[d.values.length - 1].y1;
});
y.domain([0, d3.max(data, function(d) { return d.total; })]);
x.domain(quarters);

// 初始化SVG画布
var svg = d3.select("body").append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

svg.append("g").attr("id", "grid");
// 绘制底部X轴
svg.append("g")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x));
// 绘制左侧Y轴
svg.append("g")
  .attr("transform", "translate(0,0)")
  .call(d3.axisLeft(y).tickFormat(d3.format(".2s")));

// 初始化Tooltip容器
var toolTip = d3.select("body").append("div")
  .attr("class", "toolTip")
  .html("toolTip")
  .style("display", "none");

// 按类别创建堆叠条形组
var project_stackedbar = svg.selectAll(".project_stackedbar")
  .data(data.reverse())
  .enter().append("g")
  .attr("class", "g")
  .attr("transform", function(d) { 
    return "translate(" + (x(d.dValues))  + ",0)"; 
  });

var bar_environment = project_stackedbar.selectAll("rect")
  .data(function(d) { return d.values; })
  .enter();
// 绘制各堆叠段矩形
bar_environment.append("rect")
  .attr("class", "rect-svg")
  .attr("x",  d => x(d.dValues))
  .attr("y", function(d) { return y(d.y1); })
  .attr("width", x.bandwidth() - 1)
  .attr("height", function(d){ return y(d.y0) - y(d.y1); })
  .style("fill", function(d) { return color(d.name); });
// 绘制各堆叠段内部数值标签
bar_environment.append("text")
  .attr("class", "text-svg")
  .attr("y", function(d) { return y(d.y1)+(y(d.y0) - y(d.y1))/2; })
  .attr("x", function(d){ return x.bandwidth()/2; })
  .attr("text-anchor","middle")
  .attr("font-size","12px")
  .attr("font-family","Helvetica")
  .style("fill", "#000000")
  .text(function(d) { return roundNumber(d.y1 - d.y0); });

// 绑定Tooltip交互事件
project_stackedbar.on("mouseover", function() { toolTip.style("display", null); })
  .on("mouseout", function() { toolTip.style("display", "none"); })
  .on("mousemove", function(d) {
    var xPosition = d3.event.pageX;
    var yPosition = d3.event.pageY;
    toolTip.style("left", xPosition + 10 + "px");
    toolTip.style("top", yPosition - 25 + "px");
    toolTip.style("position", "absolute");
    toolTip.style("display", "inline-block");
    
    // 获取当前悬停元素的绑定数据
    var elements = document.querySelectorAll(':hover');
    let element = elements[elements.length - 1].__data__
    let value = element.y1 - element.y0
    // 按指标类型填充Tooltip展示内容
    if(element.name === "m1Values"){
      toolTip.html("Region" + ": " + (d.dValues) +
        "<br>"+ "Sum(Sales)" + ": " + roundNumber(value));
    } else if(element.name ==="m2Values"){
      toolTip.html("Region" + ": " + (d.dValues) +
        "<br>"+ "Sum(Margin)" + ": " + roundNumber(value));
    }else{
      toolTip.html("Region" + ": " + (d.dValues) +
        "<br>"+ "Sum(GrossSales)" + ": " + roundNumber(value));
    }
  });

// 绘制条形顶部的总数值标签
var totalLabels = svg.append('g').attr('class', 'totals');
totalLabels.selectAll('.total')
  .data(data)
  .enter().append('text')
  .attr('class', 'total')
  .attr("x", function(d) { return x(d.dValues) + x.bandwidth()/2; })
  .attr("y", function(d) { return y(d.total) - 5;  })
  .attr("text-anchor","middle")
  .attr("font-size","12px")
  .attr("font-family","Helvetica")
  .style("fill", "#000000")
  .text(function(d) { return roundNumber(d.total); });

// 绘制图例
var legend = svg.append("g")
  .attr("font-family", "Helvetica")
  .attr("font-size", 10)
  .attr("text-anchor", "end")
  .selectAll("g")
  .data(["Sum(Sales)", "Sum(Margin)", "Sum(GrossSales)"])
  .enter().append("g")
  .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

legend.append("rect")
  .attr("x", width - 19)
  .attr("width", 19)
  .attr("height", 19)
  .attr("fill", color);

legend.append("text")
  .attr("x", width - 24)
  .attr("y", 9.5)
  .attr("dy", "0.32em")
  .text(function(d) { return d; });

console.log("Stacked BarChart completed");

样式定义(CSS)

div.toolTip{
  position: absolute;
  background-color: white;
  opacity: 0.9;
  border: 1px solid #c9c9c9;
  padding: 10px;
}
.gridlines line {
  stroke: lightgrey;
  stroke-opacity: 0.7;
  shape-rendering: crispEdges;
}
.gridlines path {
  stroke-width: 0;
}
body {
  font-family: 'Open Sans', sans-serif;
}

依赖引入(HTML)

<script src="https://d3js.org/d3.v4.min.js"></script>

待实现目标

调整Tooltip定位逻辑,使其在Qlik Sense容器环境下也能正确跟随鼠标位置,对齐到用户悬停的对应堆叠条形段。

内容的提问来源于stack exchange,提问作者NikSp

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.26 00:36:04