[英]Animate objects in force layout in D3.js
我需要創建一個數據可視化對象,看起來像一堆漂浮的氣泡,氣泡中包含文本。
我有一個部分工作的示例,該示例使用此處准備的模擬數據: JSfiddle
// helpers
var random = function(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
};
// mock data
var colors = [
{
fill: 'rgba(242,216,28,0.3)',
stroke: 'rgba(242,216,28,1)'
},
{
fill: 'rgba(207,203,196,0.3)',
stroke: 'rgba(207,203,196,1)'
},
{
fill: 'rgba(0,0,0,0.2)',
stroke: 'rgba(100,100,100,1)'
}
];
var data = [];
for(var j = 0; j <= 2; j++) {
for(var i = 0; i <= 4; i++) {
var text = 'text' + i;
var category = 'category' + j;
var r = random(50, 100);
data.push({
text: text,
category: category,
r: r,
r_change_1: r + random(-20, 20),
r_change_2: r + random(-20, 20),
fill: colors[j].fill,
stroke: colors[j].stroke
});
}
}
// mock debug
//console.table(data);
// collision detection
// derived from http://bl.ocks.org/mbostock/1748247
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.r + 10,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.r * 2;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// initialize
var container = d3.select('.bubble-cloud');
var $container = $('.bubble-cloud');
var containerWidth = $container.width();
var containerHeight = $container.height();
var svgContainer = container
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
// prepare layout
var force = d3.layout
.force()
.size([containerWidth, containerHeight])
.gravity(0)
.charge(0)
;
// load data
force.nodes(data)
.start()
;
// create item groups
var node = svgContainer.selectAll('.node')
.data(data)
.enter()
.append('g')
.attr('class', 'node')
.call(force.drag);
// create circles
node.append('circle')
.classed('circle', true)
.attr('r', function (d) {
return d.r;
})
.style('fill', function (d) {
return d.fill;
})
.style('stroke', function (d) {
return d.stroke;
});
// create labels
node.append('text')
.text(function(d) {
return d.text
})
.classed('text', true)
.style({
'fill': '#ffffff',
'text-anchor': 'middle',
'font-size': '12px',
'font-weight': 'bold',
'font-family': 'Tahoma, Arial, sans-serif'
})
;
node.append('text')
.text(function(d) {
return d.category
})
.classed('category', true)
.style({
'fill': '#ffffff',
'font-family': 'Tahoma, Arial, sans-serif',
'text-anchor': 'middle',
'font-size': '9px'
})
;
node.append('line')
.classed('line', true)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', 50)
.attr('y2', 0)
.attr('stroke-width', 1)
.attr('stroke', function (d) {
return d.stroke;
})
;
// put circle into movement
force.on('tick', function(){
d3.selectAll('circle')
.each(collide(.5))
.attr('cx', function (d) {
// boundaries
if(d.x <= d.r) {
d.x = d.r + 1;
}
if(d.x >= containerWidth - d.r) {
d.x = containerWidth - d.r - 1;
}
return d.x;
})
.attr('cy', function (d) {
// boundaries
if(d.y <= d.r) {
d.y = d.r + 1;
}
if(d.y >= containerHeight - d.r) {
d.y = containerHeight - d.r - 1;
}
return d.y;
});
d3.selectAll('line')
.attr('x1', function (d) {
return d.x - d.r + 10;
})
.attr('y1', function (d) {
return d.y;
})
.attr('x2', function (d) {
return d.x + d.r - 10;
})
.attr('y2', function (d) {
return d.y;
});
d3.selectAll('.text')
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y - 10;
});
d3.selectAll('.category')
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y + 20;
});
});
// animate
var interval = setInterval(function(){
// moving of the circles
// ...
}, 5 * 1000);
但是我現在面臨動畫問題。 我無法弄清楚如何在力圖中為節點設置動畫 。 我試圖調整數據對象的值,然后在setInterval方法中調用.tick()方法,但是它沒有幫助。 我正在使用D3強制布局。
我的問題是:
如何使氣泡在屏幕上“漂浮”,即如何對其進行動畫處理?
如何為圓半徑的變化制作動畫?
感謝您的想法。
其實我覺得這個感覺更好...
r = d.rt + 10
更改為r = d.rt + 10
r = d.rt + rmax
以加強重疊控制 // helpers var random = function(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); }, metrics = d3.select('.bubble-cloud').append("div") .attr("id", "metrics") .style({"white-space": "pre", "font-size": "8px"}), elapsedTime = outputs.ElapsedTime("#metrics", { border: 0, margin: 0, "box-sizing": "border-box", padding: "0 0 0 6px", background: "black", "color": "orange" }) .message(function(value) { var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap) return 'alpha:' + d3.format(" >7,.3f")(value) + '\\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps" }), hist = d3.ui.FpsMeter("#metrics", {display: "inline-block"}, { height: 8, width: 100, values: function(d){return 1/d}, domain: [0, 60] }), // mock data colors = [ { fill: 'rgba(242,216,28,0.3)', stroke: 'rgba(242,216,28,1)' }, { fill: 'rgba(207,203,196,0.3)', stroke: 'rgba(207,203,196,1)' }, { fill: 'rgba(0,0,0,0.2)', stroke: 'rgba(100,100,100,1)' } ]; // initialize var container = d3.select('.bubble-cloud'); var $container = $('.bubble-cloud'); var containerWidth = 600; var containerHeight = 180 - elapsedTime.selection.node().clientHeight; var svgContainer = container .append('svg') .attr('width', containerWidth) .attr('height', containerHeight); var data = [], rmin = 15, rmax = 30; d3.range(0, 3).forEach(function(j){ d3.range(0, 6).forEach(function(i){ var r = random(rmin, rmax); data.push({ text: 'text' + i, category: 'category' + j, x: random(rmax, containerWidth - rmax), y: random(rmax, containerHeight - rmax), r: r, fill: colors[j].fill, stroke: colors[j].stroke, get v() { var d = this; return {x: dx - d.px || 0, y: dy - d.py || 0} }, set v(v) { var d = this; d.px = dx - vx; d.py = dy - vy; }, get s() { var v = this.v; return Math.sqrt(vx * vx + vy * vy) }, set s(s1){ var s0 = this.s, v0 = this.v; if(!v0 || s0 == 0) { var theta = Math.random() * Math.PI * 2; this.v = {x: Math.cos(theta) * s1, y: Math.sin(theta) * s1} } else this.v = {x: v0.x * s1/s0, y: v0.y * s1/s0}; }, set sx(s) { this.v = {x: s, y: this.vy} }, set sy(s) { this.v = {y: s, x: this.vx} }, }); }) }); // collision detection // derived from http://bl.ocks.org/mbostock/1748247 function collide(alpha) { var quadtree = d3.geom.quadtree(data); return function(d) { var r = d.rt + rmax, nx1 = dx - r, nx2 = dx + r, ny1 = dy - r, ny2 = dy + r; quadtree.visit(function(quad, x1, y1, x2, y2) { if (quad.point && (quad.point !== d)) { var x = dx - quad.point.x, y = dy - quad.point.y, l = Math.sqrt(x * x + y * y), r = d.rt + quad.point.rt; if (l < r) { l = (l - r) / l * (1 + alpha); dx -= x *= l; dy -= y *= l; quad.point.x += x; quad.point.y += y; } } return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; }); }; } // prepare layout var force = d3.layout .force() .size([containerWidth, containerHeight]) .gravity(0.001) .charge(0) .friction(.8) .on("start", function() { elapsedTime.start(100); }); // load data force.nodes(data) .start(); // create item groups var node = svgContainer.selectAll('.node') .data(data) .enter() .append('g') .attr('class', 'node') .call(force.drag); // create circles var circles = node.append('circle') .classed('circle', true) .attr('r', function (d) { return dr; }) .style('fill', function (d) { return d.fill; }) .style('stroke', function (d) { return d.stroke; }) .each(function(d){ // add dynamic r getter var n= d3.select(this); Object.defineProperty(d, "rt", {get: function(){ return +n.attr("r") }}) }); // create labels node.append('text') .text(function(d) { return d.text }) .classed('text', true) .style({ 'fill': '#ffffff', 'text-anchor': 'middle', 'font-size': '6px', 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-family': 'Tahoma, Arial, sans-serif' }) .attr('x', function (d) { return 0; }) .attr('y', function (d) { return - rmax/5; }); node.append('text') .text(function(d) { return d.category }) .classed('category', true) .style({ 'fill': '#ffffff', 'font-family': 'Tahoma, Arial, sans-serif', 'text-anchor': 'middle', 'font-size': '4px' }) .attr('x', function (d) { return 0; }) .attr('y', function (d) { return rmax/4; }); var lines = node.append('line') .classed('line', true) .attr({ x1: function (d) { return - dr + rmax/10; }, y1: function (d) { return 0; }, x2: function (d) { return dr - rmax/10; }, y2: function (d) { return 0; } }) .attr('stroke-width', 1) .attr('stroke', function (d) { return d.stroke; }) .each(function(d){ // add dynamic x getter var n= d3.select(this); Object.defineProperty(d, "lxt", {get: function(){ return {x1: +n.attr("x1"), x2: +n.attr("x2")} }}) }); // put circle into movement force.on('tick', function t(e){ var s0 = 0.25, k = 0.3; a = e.alpha ? e.alpha : force.alpha(); elapsedTime.mark(a); if(elapsedTime.aveLap.history.length) hist(elapsedTime.aveLap.history); for ( var i = 0; i < 3; i++) { circles .each(collide(a)) .each(function(d) { var moreThan, v0; // boundaries //reflect off the edges of the container // check for boundary collisions and reverse velocity if necessary if((moreThan = dx > (containerWidth - d.rt)) || dx < d.rt) { d.escaped |= 2; // if the object is outside the boundaries // manage the sign of its x velocity component to ensure it is moving back into the bounds if(~~dvx) d.sx = dvx * (moreThan && dvx > 0 || !moreThan && dvx < 0 ? -1 : 1); // if vx is too small, then steer it back in else d.sx = (~~Math.abs(dvy) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1); // clear the boundary without affecting the velocity v0 = dv; dx = moreThan ? containerWidth - d.rt : d.rt; dv = v0; // add a bit of hysteresis to quench limit cycles } else if (dx < (containerWidth - 2*d.rt) && dx > 2*d.rt) d.escaped &= ~2; if((moreThan = dy > (containerHeight - d.rt)) || dy < d.rt) { d.escaped |= 4; if(~~dvy) d.sy = dvy * (moreThan && dvy > 0 || !moreThan && dvy < 0 ? -1 : 1); else d.sy = (~~Math.abs(dvx) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1); v0 = dv; dy = moreThan ? containerHeight - d.rt : d.rt; dv = v0; } else if (dy < (containerHeight - 2*d.rt) && dy > 2*d.rt) d.escaped &= ~4; }); } // regulate the speed of the circles data.forEach(function reg(d){ if(!d.escaped) ds = (s0 - ds * k) / (1 - k); }); node.attr("transform", function position(d){return "translate(" + [dx, dy] + ")"}); force.alpha(0.05); }); // animate window.setInterval(function(){ var tinfl = 3000, tdefl = 1000, inflate = "elastic", deflate = "cubic-out"; for(var i = 0; i < data.length; i++) { if(Math.random()>0.8) data[i].r = random(rmin,rmax); } var changes = circles.filter(function(d){return dr != d.rt}); changes.filter(function(d){return dr > d.rt}) .transition("r").duration(tinfl).ease(inflate) .attr('r', function (d) { return dr; }); changes.filter(function(d){return dr < d.rt}) .transition("r").duration(tdefl).ease(deflate) .attr('r', function (d) { return dr; }); // this runs with an error of less than 1% of rmax changes = lines.filter(function(d){return dr != d.rt}); changes.filter(function(d){return dr > d.rt}) .transition("l").duration(tinfl).ease(inflate) .attr({ x1: function lx1(d) { return -dr + rmax / 10; }, x2: function lx2(d) { return dr - rmax / 10; } }); changes.filter(function(d){return dr < d.rt}) .transition("l").duration(tdefl).ease(deflate) .attr({ x1: function lx1(d) { return -dr + rmax / 10; }, x2: function lx2(d) { return dr - rmax / 10; } }); }, 2 * 500);
body { background: black; margin:0; padding:0; } .bubble-cloud { background: url("http://dummyimage.com/100x100/111/333?text=sample") 0 0; width: 600px; height: 190px; overflow: hidden; position: relative; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script> <script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script> <script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform.js"></script> <script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.js"></script> <link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.css"> <div class="bubble-cloud"></div>
我喜歡將此公式用於間距動態...
l = (l - r) / l * (1+ alpha);
然后使用大約0.05的alpha
在我看來,不需要重力或電荷,我唯一要做的就是將摩擦設置為1。這意味着可以保持速度,但是如果您的客戶感到不適,可以將其降低到0.99。
編輯:
更改為更柔和更正確的碰撞模型
l = (l - r) / l * (1/2 + alpha);
還增加了一點重力使其變得“像雲一樣”和摩擦(請參見上文)
我也嘗試過使用CSS過渡,但至少可以說對SVG元素的支持似乎很少。
circle
半徑,但不適用於chrome(45.0)和Opera中的line
在IE 11和FF(40.0.3)中,沒有CSS過渡對我有用
我會對瀏覽器兼容性的任何反饋都感興趣,因為我在互聯網上找不到太多有關此的信息。
我在此背面進行了Velocity.js的實驗,我認為在過渡時我更喜歡它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.