尝试画布
你有27000个节点。这可能是SVG性能下降的主要原因,画布开始真正发光。当然,Canvas不像SVG那样有状态,它只是像素,没有好的元素可以在DOM中鼠标悬停,告诉您它们在哪里和什么地方。但是,有办法解决这个缺点,这样我们就可以保持速度和互动能力。
对于使用您的代码片段进行的初始渲染,我的平均渲染时间约为440ms。但是,通过canvas的魔力,我可以以约103ms的平均渲染时间渲染相同的热图。这些节省可以应用于缩放、动画等。
对于像省略号这样的非常小的事情,存在混淆问题的风险,与SVG相比,canvas很难解决这些问题,尽管每个浏览器呈现这些问题的方式会有所不同
设计含义
使用画布,我们可以保持与SVG一样的输入/退出/更新周期,但我们也有选择放弃它。有时,输入/退出/更新周期对画布非常好:转换、动态数据、HiRracic数据等。我以前花费了一些时间来研究D3与画布和SVG之间的一些高级差异。
here
.
对于我的答案,我们将离开进入周期。当我们想要更新可视化时,我们只是根据数据数组本身重新绘制所有内容。
绘制热图
我用矩形是为了简洁。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时),或用于缩放/平移事件(之后每次调用此函数)。
斧头呢?
D3轴用于SVG。所以,我刚刚在画布元素的上方叠加了一个SVG,它绝对定位并在上面的SVG上禁用鼠标事件。
说到轴,我只有一个绘图功能(更新/初始绘图之间没有区别),所以我从一开始就使用参考x比例和渲染x比例,而不是在更新功能中创建一个一次性的重缩放x比例
现在我有了画布,我该如何与它交互?
我们可以使用以下几种方法获取像素位置并将其转换为特定的基准:
-
使用Voronoi图(使用.find方法定位基准)
-
使用力布局(也使用.find方法定位基准)
-
使用隐藏画布(使用像素颜色指示基准索引)
-
使用比例尺的反转函数(当数据被网格化时)
第三个选项可能是最常见的选项之一,虽然前两个看起来很相似,但find方法在内部是不同的(voronoi neighbors vs quad tree)。在这种情况下,最后一种方法相当合适:我们有一个数据网格,我们可以反转鼠标坐标来获取行和列数据。基于您的代码片段,可能如下所示:
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在画布上移动时会触发一次,我们需要不断更新,如果我们想隐藏工具提示,我们只需检查所选像素是否为白色:
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 */ }
例子
我已经明确地提到了上面的大部分更改,但是我在下面做了一些额外的更改。首先,我需要选择画布,定位它,获取上下文等。我还将矩形替换为椭圆,因此定位有点不同(但您还有其他的定位问题,从使用线性比例(椭圆质心可以落在svg的边缘,原样),我没有修改这个来考虑椭圆/矩形的宽度/高度。这个比例问题与我没有修改的问题相差甚远。
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>