D3:慢速缩放热图
D3: slow zoomable heatmap
我有这张可缩放的热图,放大或缩小时看起来太慢了。有什么可以使它成为 faster/smoother 或者它只是太多点,这是我能拥有的最好的。我想知道是否有一些技巧可以让浏览器更轻便,同时保持工具提示等增强功能。或者也许我处理缩放功能的代码不是很好。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Bar Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
svg.selectAll("ellipse")
.attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", function(d) {
return rescaleX(d.day);
})
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", function(d) {
return (dotWidth * d3.event.transform.k);
})
.attr("fill", function(d) {
return colorScale(d.tOutC);
});
}
</script>
</body>
</html>
谢谢
以下所有综合建议的结果并不完美,但主观上稍微好一点:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Bar Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
// .on("zoom", zoomed);
.on("end", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
var scaledRadius = dotWidth * d3.event.transform.k;
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
svg.selectAll("ellipse")
// .attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", d => scaledCxes[d.day])
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", scaledRadius)
// .attr("fill", function(d) {
// return colorScale(d.tOutC);
// });
}
</script>
</body>
</html>
- 使用
on("end", zoomed)
代替 on("zoom", zoomed)
:
我们可以尝试的第一件事是仅在缩放事件结束时激活缩放更改,以免在单个缩放事件期间发生这些非确定性更新跳跃。它具有降低所需处理的效果,因为只发生一次计算,并且它消除了全局跳跃的不适:
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([ [80, 20], [width, height] ])
.on("end", zoomed); // instead of .on("zoom", zoomed);
- 删除在缩放期间保持不变的事物的更新:
我们还可以从节点更新中删除保持不变的东西,例如在缩放期间保持不变的圆圈颜色 .attr("fill", function(d) { return colorScale(d.tOutC); });
和 .attr('clip-path', 'url(#clip)')
。
- 只计算一次,多次使用:
缩放后的新圆半径只能计算一次而不是27K次,因为它对所有圆都相同:
var scaledRadius = dotWidth * d3.event.transform.k;
.attr("rx", scaledRadius)
对于 x 位置也是如此,我们可以对每个可能的 x 值计算一次(360 次)并将其存储在一个数组中以在常数时间内访问它们,而不是计算 27K 次:
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
.attr("cx", d => scaledCxes[d.day])
最后一个明显的选择是减少节点数量,因为这是问题的根源!
如果缩放范围更大,我也会建议过滤节点不再可见。
解决方案不是为缩放更新所有点,而是将缩放变换应用于包含点的组。
组的剪裁需要在另一个父 g
heatDotsGroup
.
上完成
y 的缩放比例已通过正则表达式替换处理(将其固定为 1),通过将 transform.y
设置为 0 来限制 y 中的平移,并根据当前比例。
允许稍微翻译过去 0
以在放大时显示完整的第一个点。
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.on("zoom", zoomed);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
heatDotsGroup.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) { return xScale(d.day); })
.attr("cy", function(d) { return yScale(d.hour); })
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) { return colorScale(d.tOutC); })
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1-d3.event.transform.k) * width );
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale(, 1)"));
}
尝试Canvas
您有 27 000 个节点。这可能是大多数 SVG 性能下降并且 Canvas 开始真正闪耀的时候。当然,Canvas 不像 SVG 那样是有状态的,它只是像素,没有漂亮的元素可以在 DOM 中鼠标悬停并告诉你它们在哪里以及它们是什么。但是,有一些方法可以解决这个缺点,这样我们就可以保持速度和交互能力。
对于使用您的代码片段进行的初始渲染,我的平均渲染时间约为 440 毫秒。但是,通过 canvas 的魔力,我可以渲染相同的热图,平均渲染时间约为 103 毫秒。这些节省可用于缩放、动画等。
对于像椭圆这样的非常小的东西,存在混叠问题的风险,与 SVG 相比,canvas 更难解决,尽管每个浏览器呈现的方式会有所不同
设计意义
使用 Canvas 我们可以像使用 SVG 一样保留 enter/exit/update 循环,但我们也可以选择放弃它。有时,enter/exit/update 循环与 canvas 配对得非常好:转换、动态数据、分层数据等。我之前花了一些时间研究 Canvas 和 SVG 之间的一些更高级别的差异关于 D3 .
对于我在这里的回答,我们将离开输入循环。当我们想要更新可视化时,我们只需根据数据数组本身重新绘制所有内容。
绘制热图
为了简洁起见,我使用了矩形。 Canvas的椭圆方法还没有完全准备好,但是你可以emulate it easily enough.
我们需要一个绘制数据集的函数。如果您将 x/y/color 硬编码到数据集中,我们可以使用非常简单的方法:
function drawNodes()
dataset.forEach(function(d) {
ctx.beginPath();
ctx.rect(d.x,d.y,width,height);
ctx.fillStyle = d.color;
ctx.fill();
})
}
但是我们需要缩放你的值,计算颜色,我们应该应用缩放。我最终得到了一个相对简单的:
function drawNodes()
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height); // erase what's there
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
这可用于初始绘制节点(当 d3.event 未定义时),或在 zoom/pan 事件上(之后每次调用此函数)。
轴呢?
d3 轴适用于 SVG。因此,我刚刚在 Canvas 元素上叠加了一个 SVG overtop,绝对定位并禁用了叠加 SVG 上的鼠标事件。
说到轴,我只有一个绘图功能(update/initial绘图没有区别),所以我从一开始就使用参考x比例尺和渲染x比例尺,而不是比在更新函数中创建一次性重新缩放的 x 比例
现在我有了一个 Canvas,我该如何与之互动?
我们可以使用几种方法获取像素位置并将其转换为特定数据:
- 使用 Voronoi 图(使用 .find 方法定位数据)
- 使用 Force 布局(也使用 .find 方法定位数据)
- 使用隐藏的Canvas(使用像素颜色指示基准索引)
- 使用刻度的反转函数(当数据网格化时)
第三个选项可能是最常见的选项之一,虽然前两个看起来相似,但查找方法在内部确实不同(voronoi 邻居与四叉树)。最后一种方法在这种情况下相当合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。根据您的代码段,可能如下所示:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
// For rounding on canvas edges:
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
console.log(x,y,index,d)
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}
*我用过mousemove,因为mouseover在canvas上移动时会触发一次,我们需要不断更新,如果我们想隐藏工具提示,我们可以检查像素selected 是白色的:
var p = ctx.getImageData(xy[0], xy[1], 1, 1).data; // pixel data:
if (!p[0] && !p[1] && !p[2]) { /* show tooltip */ }
else { /* hide tooltip */ }
例子
我已经在上面明确提到了大部分更改,但我在下面还做了一些额外的更改。首先,我需要 select canvas、定位它、获取上下文等。我还用椭圆交换了矩形,所以定位有点不同(但你还有其他定位问题使用线性比例(椭圆质心可以按原样落在 svg 的边缘),我没有修改它来解释 ellipses/rects 的 width/height。这个比例问题已经足够了我没修改的问题
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) { return d.day; }) - d3.min(dataset, function(d) { return d.day; });
var hours = d3.max(dataset, function(d) { return d.hour; }) - d3.min(dataset, function(d) { return d.hour; });
var tMin = d3.min(dataset, function(d) { return d.tOutC; }), tMax = d3.max(dataset, function(d) { return d.tOutC; });
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = { top: 20, right: 25, bottom: 40, left: 25 },
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var xScaleRef = xScale.copy();
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([height,0]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) { return d.tOutC; })])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[0,0],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG & Canvas:
var canvas = d3.select("#chart")
.append("canvas")
.attr("width", width)
.attr("height", height)
.style("left", margin.left + "px")
.style("top", margin.top + "px")
.style("position","absolute")
.on("mousemove", mousemove)
.on("mouseout", mouseout);
var svg = d3.select("#chart")
.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]+")");
var ctx = canvas.node().getContext("2d");
canvas.call(zoom);
// Initial Draw:
drawNodes(dataset);
//Create Axes:
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Handle Zoom:
function zoomed() {
// rescale the x Axis:
xScale = d3.event.transform.rescaleX(xScaleRef); // Use Reference Scale.
// Redraw the x Axis:
renderXAxis.call(xAxis.scale(xScale));
// Clear and redraw the nodes:
drawNodes();
}
// Draw nodes:
function drawNodes() {
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height);
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
// Mouse movement:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}
function mouseout() {
$("#tooltip").animate({duration: 500}).css("opacity",0);
};
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
svg {
position: absolute;
top: 0;
left:0;
pointer-events: none;
}
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<div id="chart" style="width: 700px; height: 500px"></div>
一定要检查 LightningChart JS 热图 - 它可以免费用于非商业用途。
这是性能最佳的热图网络图表的性能比较https://github.com/Arction/javascript-charts-performance-comparison-heatmaps
正如您在那边看到的那样,我们正在谈论可视化热图,这些热图在数十亿个数据点的范围内,并且用户交互仍然可以正常工作。
// Source https://www.arction.com/lightningchart-js-interactive-examples/edit/lcjs-example-0800-heatmapGrid.html
/*
* LightningChartJS example that showcases a simple XY line series.
*/
// Extract required parts from LightningChartJS.
const { lightningChart, PalettedFill, LUT, ColorRGBA, emptyLine, Themes } =
lcjs;
const { createWaterDropDataGenerator } = xydata;
// Specify the resolution used for the heatmap.
const resolutionX = 1000;
const resolutionY = 1000;
// Create a XY Chart.
const chart = lightningChart()
.ChartXY({
// theme: Themes.darkGold
})
.setTitle(
`Heatmap Grid Series ${resolutionX}x${resolutionY} (${(
(resolutionX * resolutionY) /
1000000
).toFixed(1)} million data points)`
)
.setPadding({ right: 40 });
// Create LUT and FillStyle
const palette = new LUT({
units: "intensity",
steps: [
{ value: 0, color: ColorRGBA(255, 255, 0) },
{ value: 30, color: ColorRGBA(255, 204, 0) },
{ value: 45, color: ColorRGBA(255, 128, 0) },
{ value: 60, color: ColorRGBA(255, 0, 0) },
],
interpolate: false,
});
// Generate heatmap data.
createWaterDropDataGenerator()
.setRows(resolutionX)
.setColumns(resolutionY)
.generate()
.then((data) => {
// Add a Heatmap to the Chart.
const heatmap = chart
.addHeatmapGridSeries({
columns: resolutionX,
rows: resolutionY,
start: { x: 0, y: 0 },
end: { x: resolutionX, y: resolutionY },
dataOrder: "columns",
})
// Color Heatmap using previously created color look up table.
.setFillStyle(new PalettedFill({ lut: palette }))
.setWireframeStyle(emptyLine)
.invalidateIntensityValues(data)
.setMouseInteractions(false);
// Add LegendBox.
const legend = chart.addLegendBox()
// Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
.setAutoDispose({
type: 'max-height',
maxHeight: 0.70,
})
.add(chart)
});
<script src="http://unpkg.com/@arction/lcjs@3.1.0/dist/lcjs.iife.js"></script>
<script src="http://unpkg.com/@arction/xydata@1.4.0/dist/xydata.iife.js"></script>
我有这张可缩放的热图,放大或缩小时看起来太慢了。有什么可以使它成为 faster/smoother 或者它只是太多点,这是我能拥有的最好的。我想知道是否有一些技巧可以让浏览器更轻便,同时保持工具提示等增强功能。或者也许我处理缩放功能的代码不是很好。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Bar Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
svg.selectAll("ellipse")
.attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", function(d) {
return rescaleX(d.day);
})
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", function(d) {
return (dotWidth * d3.event.transform.k);
})
.attr("fill", function(d) {
return colorScale(d.tOutC);
});
}
</script>
</body>
</html>
谢谢
以下所有综合建议的结果并不完美,但主观上稍微好一点:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Bar Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<div id="chart" style="width: 700px; height: 500px"></div>
<script>
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) {
return d.day;
}) -
d3.min(dataset, function(d) {
return d.day;
});
var hours = d3.max(dataset, function(d) {
return d.hour;
}) -
d3.min(dataset, function(d) {
return d.hour;
});
var tMin = d3.min(dataset, function(d) {
return d.tOutC;
}),
tMax = d3.max(dataset, function(d) {
return d.tOutC;
});
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 25
},
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.tOutC;
})])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
// Define Y axis
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[80, 20],
[width, height]
])
// .on("zoom", zoomed);
.on("end", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Heatmap dots
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return xScale(d.day);
})
.attr("cy", function(d) {
return yScale(d.hour);
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.tOutC);
})
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
update();
}
function update() {
// update: cache rescaleX value
var rescaleX = d3.event.transform.rescaleX(xScale);
var scaledRadius = dotWidth * d3.event.transform.k;
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
svg.selectAll("ellipse")
// .attr('clip-path', 'url(#clip)')
// update: apply rescaleX value
.attr("cx", d => scaledCxes[d.day])
// .attr("cy", function(d) {
// return yScale(d.hour);
// })
// update: apply rescaleX value
.attr("rx", scaledRadius)
// .attr("fill", function(d) {
// return colorScale(d.tOutC);
// });
}
</script>
</body>
</html>
- 使用
on("end", zoomed)
代替on("zoom", zoomed)
:
我们可以尝试的第一件事是仅在缩放事件结束时激活缩放更改,以免在单个缩放事件期间发生这些非确定性更新跳跃。它具有降低所需处理的效果,因为只发生一次计算,并且它消除了全局跳跃的不适:
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([ [80, 20], [width, height] ])
.on("end", zoomed); // instead of .on("zoom", zoomed);
- 删除在缩放期间保持不变的事物的更新:
我们还可以从节点更新中删除保持不变的东西,例如在缩放期间保持不变的圆圈颜色 .attr("fill", function(d) { return colorScale(d.tOutC); });
和 .attr('clip-path', 'url(#clip)')
。
- 只计算一次,多次使用:
缩放后的新圆半径只能计算一次而不是27K次,因为它对所有圆都相同:
var scaledRadius = dotWidth * d3.event.transform.k;
.attr("rx", scaledRadius)
对于 x 位置也是如此,我们可以对每个可能的 x 值计算一次(360 次)并将其存储在一个数组中以在常数时间内访问它们,而不是计算 27K 次:
var scaledCxes = [...Array(360).keys()].map(i => rescaleX(i));
.attr("cx", d => scaledCxes[d.day])
最后一个明显的选择是减少节点数量,因为这是问题的根源!
如果缩放范围更大,我也会建议过滤节点不再可见。
解决方案不是为缩放更新所有点,而是将缩放变换应用于包含点的组。
组的剪裁需要在另一个父 g
heatDotsGroup
.
y 的缩放比例已通过正则表达式替换处理(将其固定为 1),通过将 transform.y
设置为 0 来限制 y 中的平移,并根据当前比例。
允许稍微翻译过去 0
以在放大时显示完整的第一个点。
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.on("zoom", zoomed);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
heatDotsGroup.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) { return xScale(d.day); })
.attr("cy", function(d) { return yScale(d.hour); })
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) { return colorScale(d.tOutC); })
.on("mouseover", function(d){
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}).on("mouseout", function(){
$("#tooltip").animate({duration: 500}).css("opacity",0);
});
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1-d3.event.transform.k) * width );
// update: rescale x axis
renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale(, 1)"));
}
尝试Canvas
您有 27 000 个节点。这可能是大多数 SVG 性能下降并且 Canvas 开始真正闪耀的时候。当然,Canvas 不像 SVG 那样是有状态的,它只是像素,没有漂亮的元素可以在 DOM 中鼠标悬停并告诉你它们在哪里以及它们是什么。但是,有一些方法可以解决这个缺点,这样我们就可以保持速度和交互能力。
对于使用您的代码片段进行的初始渲染,我的平均渲染时间约为 440 毫秒。但是,通过 canvas 的魔力,我可以渲染相同的热图,平均渲染时间约为 103 毫秒。这些节省可用于缩放、动画等。
对于像椭圆这样的非常小的东西,存在混叠问题的风险,与 SVG 相比,canvas 更难解决,尽管每个浏览器呈现的方式会有所不同
设计意义
使用 Canvas 我们可以像使用 SVG 一样保留 enter/exit/update 循环,但我们也可以选择放弃它。有时,enter/exit/update 循环与 canvas 配对得非常好:转换、动态数据、分层数据等。我之前花了一些时间研究 Canvas 和 SVG 之间的一些更高级别的差异关于 D3
对于我在这里的回答,我们将离开输入循环。当我们想要更新可视化时,我们只需根据数据数组本身重新绘制所有内容。
绘制热图
为了简洁起见,我使用了矩形。 Canvas的椭圆方法还没有完全准备好,但是你可以emulate it easily enough.
我们需要一个绘制数据集的函数。如果您将 x/y/color 硬编码到数据集中,我们可以使用非常简单的方法:
function drawNodes()
dataset.forEach(function(d) {
ctx.beginPath();
ctx.rect(d.x,d.y,width,height);
ctx.fillStyle = d.color;
ctx.fill();
})
}
但是我们需要缩放你的值,计算颜色,我们应该应用缩放。我最终得到了一个相对简单的:
function drawNodes()
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height); // erase what's there
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
这可用于初始绘制节点(当 d3.event 未定义时),或在 zoom/pan 事件上(之后每次调用此函数)。
轴呢?
d3 轴适用于 SVG。因此,我刚刚在 Canvas 元素上叠加了一个 SVG overtop,绝对定位并禁用了叠加 SVG 上的鼠标事件。
说到轴,我只有一个绘图功能(update/initial绘图没有区别),所以我从一开始就使用参考x比例尺和渲染x比例尺,而不是比在更新函数中创建一次性重新缩放的 x 比例
现在我有了一个 Canvas,我该如何与之互动?
我们可以使用几种方法获取像素位置并将其转换为特定数据:
- 使用 Voronoi 图(使用 .find 方法定位数据)
- 使用 Force 布局(也使用 .find 方法定位数据)
- 使用隐藏的Canvas(使用像素颜色指示基准索引)
- 使用刻度的反转函数(当数据网格化时)
第三个选项可能是最常见的选项之一,虽然前两个看起来相似,但查找方法在内部确实不同(voronoi 邻居与四叉树)。最后一种方法在这种情况下相当合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。根据您的代码段,可能如下所示:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
// For rounding on canvas edges:
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
console.log(x,y,index,d)
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}
*我用过mousemove,因为mouseover在canvas上移动时会触发一次,我们需要不断更新,如果我们想隐藏工具提示,我们可以检查像素selected 是白色的:
var p = ctx.getImageData(xy[0], xy[1], 1, 1).data; // pixel data:
if (!p[0] && !p[1] && !p[2]) { /* show tooltip */ }
else { /* hide tooltip */ }
例子
我已经在上面明确提到了大部分更改,但我在下面还做了一些额外的更改。首先,我需要 select canvas、定位它、获取上下文等。我还用椭圆交换了矩形,所以定位有点不同(但你还有其他定位问题使用线性比例(椭圆质心可以按原样落在 svg 的边缘),我没有修改它来解释 ellipses/rects 的 width/height。这个比例问题已经足够了我没修改的问题
var dataset = [];
for (let i = 1; i < 360; i++) {
for (j = 1; j < 75; j++) {
dataset.push({
day: i,
hour: j,
tOutC: Math.random() * 25,
})
}
};
var days = d3.max(dataset, function(d) { return d.day; }) - d3.min(dataset, function(d) { return d.day; });
var hours = d3.max(dataset, function(d) { return d.hour; }) - d3.min(dataset, function(d) { return d.hour; });
var tMin = d3.min(dataset, function(d) { return d.tOutC; }), tMax = d3.max(dataset, function(d) { return d.tOutC; });
var dotWidth = 1,
dotHeight = 3,
dotSpacing = 0.5;
var margin = { top: 20, right: 25, bottom: 40, left: 25 },
width = (dotWidth * 2 + dotSpacing) * days,
height = (dotHeight * 2 + dotSpacing) * hours;
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];
var xScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.day}))
.range([0, width]);
var xScaleRef = xScale.copy();
var yScale = d3.scaleLinear()
.domain(d3.extent(dataset, function(d){return d.hour}))
.range([height,0]);
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) { return d.tOutC; })])
.range(colors);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.translateExtent([
[0,0],
[width, height]
])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG & Canvas:
var canvas = d3.select("#chart")
.append("canvas")
.attr("width", width)
.attr("height", height)
.style("left", margin.left + "px")
.style("top", margin.top + "px")
.style("position","absolute")
.on("mousemove", mousemove)
.on("mouseout", mouseout);
var svg = d3.select("#chart")
.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]+")");
var ctx = canvas.node().getContext("2d");
canvas.call(zoom);
// Initial Draw:
drawNodes(dataset);
//Create Axes:
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + yScale(0) + ")")
.call(xAxis)
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Handle Zoom:
function zoomed() {
// rescale the x Axis:
xScale = d3.event.transform.rescaleX(xScaleRef); // Use Reference Scale.
// Redraw the x Axis:
renderXAxis.call(xAxis.scale(xScale));
// Clear and redraw the nodes:
drawNodes();
}
// Draw nodes:
function drawNodes() {
var k = d3.event ? d3.event.transform.k : 1;
var dw = dotWidth * k;
ctx.clearRect(0,0,width,height);
dataset.forEach(function(d) {
var x = xScale(d.day);
var y = yScale(d.hour);
var fill = colorScale(d.tOutC);
ctx.beginPath();
ctx.rect(x,y,dw,dotHeight);
ctx.fillStyle = fill;
ctx.strokeStyle = fill;
ctx.stroke();
ctx.fill();
})
}
// Mouse movement:
function mousemove() {
var xy = d3.mouse(this);
var x = Math.round(xScale.invert(xy[0]));
var y = Math.round(yScale.invert(xy[1]));
if(x > xScaleRef.domain()[1]) x = xScaleRef.domain()[1];
if(x < xScaleRef.domain()[0]) x = xScaleRef.domain()[0];
if(y > yScale.domain()[1]) y = yScale.domain()[1];
if(y < yScale.domain()[0]) y = yScale.domain()[0];
var index = --x*74 + y-1; // minus ones for non zero indexed x,y values.
var d = dataset[index];
$("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
var xpos = d3.event.pageX +10;
var ypos = d3.event.pageY +20;
$("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
}
function mouseout() {
$("#tooltip").animate({duration: 500}).css("opacity",0);
};
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
svg {
position: absolute;
top: 0;
left:0;
pointer-events: none;
}
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<div id="chart" style="width: 700px; height: 500px"></div>
一定要检查 LightningChart JS 热图 - 它可以免费用于非商业用途。
这是性能最佳的热图网络图表的性能比较https://github.com/Arction/javascript-charts-performance-comparison-heatmaps
正如您在那边看到的那样,我们正在谈论可视化热图,这些热图在数十亿个数据点的范围内,并且用户交互仍然可以正常工作。
// Source https://www.arction.com/lightningchart-js-interactive-examples/edit/lcjs-example-0800-heatmapGrid.html
/*
* LightningChartJS example that showcases a simple XY line series.
*/
// Extract required parts from LightningChartJS.
const { lightningChart, PalettedFill, LUT, ColorRGBA, emptyLine, Themes } =
lcjs;
const { createWaterDropDataGenerator } = xydata;
// Specify the resolution used for the heatmap.
const resolutionX = 1000;
const resolutionY = 1000;
// Create a XY Chart.
const chart = lightningChart()
.ChartXY({
// theme: Themes.darkGold
})
.setTitle(
`Heatmap Grid Series ${resolutionX}x${resolutionY} (${(
(resolutionX * resolutionY) /
1000000
).toFixed(1)} million data points)`
)
.setPadding({ right: 40 });
// Create LUT and FillStyle
const palette = new LUT({
units: "intensity",
steps: [
{ value: 0, color: ColorRGBA(255, 255, 0) },
{ value: 30, color: ColorRGBA(255, 204, 0) },
{ value: 45, color: ColorRGBA(255, 128, 0) },
{ value: 60, color: ColorRGBA(255, 0, 0) },
],
interpolate: false,
});
// Generate heatmap data.
createWaterDropDataGenerator()
.setRows(resolutionX)
.setColumns(resolutionY)
.generate()
.then((data) => {
// Add a Heatmap to the Chart.
const heatmap = chart
.addHeatmapGridSeries({
columns: resolutionX,
rows: resolutionY,
start: { x: 0, y: 0 },
end: { x: resolutionX, y: resolutionY },
dataOrder: "columns",
})
// Color Heatmap using previously created color look up table.
.setFillStyle(new PalettedFill({ lut: palette }))
.setWireframeStyle(emptyLine)
.invalidateIntensityValues(data)
.setMouseInteractions(false);
// Add LegendBox.
const legend = chart.addLegendBox()
// Dispose example UI elements automatically if they take too much space. This is to avoid bad UI on mobile / etc. devices.
.setAutoDispose({
type: 'max-height',
maxHeight: 0.70,
})
.add(chart)
});
<script src="http://unpkg.com/@arction/lcjs@3.1.0/dist/lcjs.iife.js"></script>
<script src="http://unpkg.com/@arction/xydata@1.4.0/dist/xydata.iife.js"></script>