D3.js 鼠标悬停在 html5 canvas 散点图上
D3.js mouseovers on html5 canvas scatter plot
假设我们绘制散点图不是使用 SVG,而是使用 canvas。
所以像这样:
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale),
colors = {};
// Update canvas
context.clearRect(0, 0, width, height);
picker.clearRect(0, 0, width, height);
points.forEach(function(p,i){
// Space out the colors a bit
var color = getColor(i * 1000 + 1);
colors[color] = p;
picker.fillStyle = "rgb(" + color + ")";
context.beginPath();
picker.beginPath();
context.arc(x(p[0]), y(p[1]), 10, 0, 2 * Math.PI);
picker.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
picker.fill();
});
现在我们如何将数据传递到鼠标悬停事件中,比如绘制工具提示?
我看到的工具提示示例都理所当然地认为您正在处理一个事件,其中数据绑定到鼠标悬停的元素。
但是 canvas 呢?
我假设您需要根据鼠标事件的 x y 坐标使用 d3.bisector
或类似的东西。
一种方法是遍历所有点并检查 x 和 y 是否与点击附近相匹配。当散点图点太多时,这肯定会很慢。{我认为在你的情况下你正在 canvas 中制作散点图只是为了解决这个问题}
另一种方法是利用四叉树。
首先我随机抽取 10000 点。
sampleData = d3.range(1000).map(function(d) {
var datapoint = {};
datapoint.id = "Sample Node " + d;
datapoint.x = Math.random() * 500;
datapoint.y = Math.random() * 500;
return datapoint;
})
像这样将散点图中的所有点存储在四叉树中。
quadtree = d3.geom.quadtree()
.extent([[0,0], [500,500]]) //here 500 is the width and height of the canvas or the max x/y range of the points in scatter chart.
.x(function(d) {return d.x})
.y(function(d) {return d.y});
将所有点传递到四叉树中:
quadData = quadtree(sampleData);
现在单击查找关联的节点数据:
quadData = quadtree(sampleData);
d3.select("canvas").on("click", function(){
found = [];
//find in the vicinity of 10 pixel around the click.
search(quadData, d3.event.pageX -10, d3.event.pageY-10, d3.event.pageX +10, d3.event.pageY+10);
var message = "";
//iterate the found and make the message
found.forEach(function(d){
message += d.id + " ";
});
alert("selected Node" + message);
var data
})
最后我的搜索功能检查四叉树矩形中的节点:
function search(quadtree, x0, y0, x3, y3) {
quadtree.visit(function(node, x1, y1, x2, y2) {
var p = node.point;
if (p) {
p.selected = (p.x >= x0) && (p.x < x3) && (p.y >= y0) && (p.y < y3);
if(p.selected){
found.push(p);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
}
点击任何一个圆圈,你会收到它包含的数据的提醒
工作代码 here
我最终使用了 Noah Veltman 建议的解决方案,如下所示:
var margin = {top: 20, right: 10, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
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 + ")");
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var xg = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var yg = svg.append("g")
.attr("class", "y axis");
var chartArea = d3.select("body").append("div")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var canvas = chartArea.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.fillStyle = "#f0f";
// Layer on top of canvas, example of selection details
var highlight = chartArea.append("svg")
.attr("width", width)
.attr("height", height)
.append("circle")
.attr("r", 7)
.classed("hidden", true);
redraw();
function redraw() {
// Randomize the scale
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale);
var tree = factory(points);
// Update canvas
context.clearRect(0, 0, width, height);
points.forEach(function(p,i){
context.beginPath();
context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
});
canvas.on("mousemove",function(){
var mouse = d3.mouse(this),
closest = tree.find([x.invert(mouse[0]), y.invert(mouse[1])]);
highlight.attr("cx", x(closest[0]))
.attr("cy", y(closest[1]));
});
canvas.on("mouseover",function(){
highlight.classed("hidden", false);
});
canvas.on("mouseout",function(){
highlight.classed("hidden", true);
});
}
function randomPoints(scale) {
// Get points
return d3.range(1000).map(function(d){
return [
Math.random() * scale,
Math.random() * scale
];
});
}
关键是使用
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
其中有一个 getter setter 函数基于每个点的 x, y 值对。
假设我们绘制散点图不是使用 SVG,而是使用 canvas。
所以像这样:
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale),
colors = {};
// Update canvas
context.clearRect(0, 0, width, height);
picker.clearRect(0, 0, width, height);
points.forEach(function(p,i){
// Space out the colors a bit
var color = getColor(i * 1000 + 1);
colors[color] = p;
picker.fillStyle = "rgb(" + color + ")";
context.beginPath();
picker.beginPath();
context.arc(x(p[0]), y(p[1]), 10, 0, 2 * Math.PI);
picker.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
picker.fill();
});
现在我们如何将数据传递到鼠标悬停事件中,比如绘制工具提示?
我看到的工具提示示例都理所当然地认为您正在处理一个事件,其中数据绑定到鼠标悬停的元素。
但是 canvas 呢?
我假设您需要根据鼠标事件的 x y 坐标使用 d3.bisector
或类似的东西。
一种方法是遍历所有点并检查 x 和 y 是否与点击附近相匹配。当散点图点太多时,这肯定会很慢。{我认为在你的情况下你正在 canvas 中制作散点图只是为了解决这个问题}
另一种方法是利用四叉树。 首先我随机抽取 10000 点。
sampleData = d3.range(1000).map(function(d) {
var datapoint = {};
datapoint.id = "Sample Node " + d;
datapoint.x = Math.random() * 500;
datapoint.y = Math.random() * 500;
return datapoint;
})
像这样将散点图中的所有点存储在四叉树中。
quadtree = d3.geom.quadtree()
.extent([[0,0], [500,500]]) //here 500 is the width and height of the canvas or the max x/y range of the points in scatter chart.
.x(function(d) {return d.x})
.y(function(d) {return d.y});
将所有点传递到四叉树中:
quadData = quadtree(sampleData);
现在单击查找关联的节点数据:
quadData = quadtree(sampleData);
d3.select("canvas").on("click", function(){
found = [];
//find in the vicinity of 10 pixel around the click.
search(quadData, d3.event.pageX -10, d3.event.pageY-10, d3.event.pageX +10, d3.event.pageY+10);
var message = "";
//iterate the found and make the message
found.forEach(function(d){
message += d.id + " ";
});
alert("selected Node" + message);
var data
})
最后我的搜索功能检查四叉树矩形中的节点:
function search(quadtree, x0, y0, x3, y3) {
quadtree.visit(function(node, x1, y1, x2, y2) {
var p = node.point;
if (p) {
p.selected = (p.x >= x0) && (p.x < x3) && (p.y >= y0) && (p.y < y3);
if(p.selected){
found.push(p);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
}
点击任何一个圆圈,你会收到它包含的数据的提醒 工作代码 here
我最终使用了 Noah Veltman 建议的解决方案,如下所示:
var margin = {top: 20, right: 10, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
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 + ")");
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var xg = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var yg = svg.append("g")
.attr("class", "y axis");
var chartArea = d3.select("body").append("div")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var canvas = chartArea.append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.fillStyle = "#f0f";
// Layer on top of canvas, example of selection details
var highlight = chartArea.append("svg")
.attr("width", width)
.attr("height", height)
.append("circle")
.attr("r", 7)
.classed("hidden", true);
redraw();
function redraw() {
// Randomize the scale
var scale = 1 + Math.floor(Math.random() * 10);
// Redraw axes
x.domain([0, scale]);
y.domain([0, scale]);
xg.call(xAxis);
yg.call(yAxis);
var points = randomPoints(scale);
var tree = factory(points);
// Update canvas
context.clearRect(0, 0, width, height);
points.forEach(function(p,i){
context.beginPath();
context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
context.fill();
});
canvas.on("mousemove",function(){
var mouse = d3.mouse(this),
closest = tree.find([x.invert(mouse[0]), y.invert(mouse[1])]);
highlight.attr("cx", x(closest[0]))
.attr("cy", y(closest[1]));
});
canvas.on("mouseover",function(){
highlight.classed("hidden", false);
});
canvas.on("mouseout",function(){
highlight.classed("hidden", true);
});
}
function randomPoints(scale) {
// Get points
return d3.range(1000).map(function(d){
return [
Math.random() * scale,
Math.random() * scale
];
});
}
关键是使用
var factory = d3.geom.quadtree()
.extent([
[0, 0],
[width, height]
]);
其中有一个 getter setter 函数基于每个点的 x, y 值对。