簡體   English   中英

在D3.js中以強制布局對對象進行動畫處理

[英]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強制布局。

我的問題是:

  • 如何使氣泡在屏幕上“漂浮”,即如何對其進行動畫處理?

  • 如何為圓半徑的變化制作動畫?

感謝您的想法。

其實我覺得這個感覺更好...


關鍵點

  1. 電荷設置為0,摩擦設置為0.9
  2. 在計時器回調中安排半徑和直線上的平行過渡
  3. 使用動態半徑計算碰撞
  4. 在節點(g元素)上使用變換將文本和行定位與節點位置解耦,僅在tick回調中調整變換x和y
  5. 刪除CSS過渡並添加d3過渡,以便您可以同步所有內容
  6. 將碰撞函數中的r = d.rt + 10更改為r = d.rt + 10 r = d.rt + rmax以加強重疊控制
  7. 閉環調速器。 即使將摩擦力設置為0.9以抑制運動,速度調節器也會使它們保持運動
  8. 使用平行過渡來協調幾何變化
  9. 增加了少量的重力

工作實例

 // 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過渡

我也嘗試過使用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM