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
相关产品推荐
相关产品推荐

