使用 gravity/collide detection/effects 将气泡图升级到 v4+
Upgrading bubble chart to v4+ with gravity/collide detection/effects
我有一个 converted d3v4 气泡图,但在 d3v3 中曾经有更多的功能,例如 gravity/charge 和碰撞检测等。当图表加载时 - 我想看到 consistent/movement 个气泡,就像看池塘里的青蛙产卵一样 - 它们通过重力彼此靠近 - 但具有 repellant/charge 类型属性。泡泡也要尽量远离边缘
Here's 我在 v3 中寻找什么:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<div class="bubblechart" data-role="bubblechart" data-width="300" data-height="300" data="">
</div>
<script>
$(document).ready(function() {
var $this = $('.bubblechart');
//console.log("rendered div now engage d3", $this);
// set el height and width etc.
var w = $this.data("width");
var h = $this.data("height");
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
function colores_google(n) {
var colores_g = ["#ff7276", "#4baad2", "#eaa2a5", "#e75763", "#a6a19e"];
return colores_g[n % colores_g.length];
}
var methods = {
el: "",
init: function(el, options) {
var clone = options["data"];
var that = this;
//console.log("clone", clone);
w = options["width"];
h = options["height"];
methods.el = el;
methods.setup(clone, w, h);
//methods.resizeChart(methods.el["selector"]);
},
resizeChart: function(selector) {
//alert(selector);
var svg = $(selector + " .bubblechart");
var aspect = svg.width() / svg.height();
var targetWidth = svg.parent().parent().width();
if (targetWidth != null) {
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
}
},
funnelData: function(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
},
getMargin: function() {
return {
top: 30,
right: 25,
bottom: 50,
left: 25
};
},
setup: function(data, w, h) {
methods.width = w;
methods.height = h;
methods.fill = d3.scale.ordinal()
.range(["#d84b2a", "#beccae", "#7aa25c", "#008000"])
var margin = methods.getMargin();
var selector = methods.el;
var padding = 50;
/*
var svg = d3.select(selector)
.append("svg")
.attr("class", "bubblechart")
.attr("width", parseInt(w + margin.left + margin.right,10))
.attr("height", parseInt(h + margin.top + margin.bottom,10))
.attr('viewBox', "0 0 "+parseInt(w + margin.left + margin.right,10)+" "+parseInt(h + margin.top + margin.bottom,10))
.attr('perserveAspectRatio', "xMinYMid")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
*/
var chart = d3.select(selector).append("svg:svg")
.attr("class", "chart")
.attr("width", w - (w / 5))
.attr("height", h)
.attr("preserveAspectRatio", "none")
.attr("viewBox", "0 0 " + (w - (w / 5)) + " " + h + "")
.append("svg:g")
.attr("class", "bubblechart")
.attr("transform", "translate(-10,0)");
methods.force = d3.layout.force()
.charge(100)
.gravity(1200)
.size([methods.width, methods.height])
var bubbleholder = chart.append("g")
.attr("class", "bubbleholder")
var bubbles = bubbleholder.append("g")
.attr("class", "bubbles")
var labelbubble = bubbleholder.append("g")
.attr("class", "labelbubble")
//add legend
var legendPaddingTop = 30;
var legend = d3.select($this[0]).append("svg:svg")
.attr("class", "legend")
.attr("width", w / 5)
.attr("height", h)
.append("svg:g")
.attr("class", "legendsection")
.attr("transform", "translate(" + ((w / 4) + padding) + "," + legendPaddingTop + ")");
var label_group = legend.append("svg:g")
.attr("class", "label_group")
.attr("transform", "translate(" + (-(w / 3) + 20) + "," + 0 + ")");
var legend_group = legend.append("svg:g")
.attr("class", "legend_group")
.attr("transform", "translate(" + (-(w / 3) - 100) + "," + 0 + ")");
//draw labels
var labels = label_group.selectAll("text.labels")
.data(data);
var legendHeight = legendPaddingTop;
var ySpace = 18;
var labelPadding = 3;
labels.enter().append("svg:text")
.attr("class", "labels")
.attr("dy", function(d, i) {
legendHeight += ySpace;
return (ySpace * i) + labelPadding;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
labels.exit().remove();
var legend = legend_group.selectAll("circle").data(data);
legend.enter().append("svg:circle")
.attr("cx", 100)
.attr("cy", function(d, i) {
return ySpace * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
//reset legend height
//console.log("optimum height for legend", legendHeight);
$this.find('.legend').attr("height", legendHeight);
//add data
data = this.funnelData(data, methods.width, methods.height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (methods.width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.style("fill", function(d, i) {
return colores_google(i);
})
.call(methods.force.drag);
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
var bubblelabels = labelbubble.selectAll("text")
.data(data);
// Enter
bubblelabels.enter()
.append("text")
.attr("class", function(d) {
var cls = "title";
if (d.count > 9) {
cls += " largetxt";
}
return cls;
})
.text(function(d) {
return d.count;
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return (d.y) + 5;
});
// Update
bubblelabels
.transition()
.delay(300)
.duration(1000)
// Exit
bubblelabels.exit()
.transition()
.duration(250)
.remove();
draw('all');
function draw(varname) {
var foci = {
"all": {
name: "All",
x: methods.width / 2,
y: methods.height / 2
}
};
methods.force.on("tick", tick(foci, varname, .55));
methods.force.start();
}
function tick(foci, varname, k) {
return function(e) {
data.forEach(function(o, i) {
var f = foci[o[varname]];
o.y += (f.y - o.y) * k * e.alpha;
o.x += (f.x - o.x) * k * e.alpha;
});
nodes
.each(collide(.1))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
bubblelabels
.each(collide(.1))
.attr("x", function(d) {
var displacementText = -5;
if (d.count > 9) {
displacementText = -14;
}
return (d.x + displacementText);
})
.attr("y", function(d) {
var displacementText = 5;
if (d.count > 9) {
displacementText = 7;
}
return (d.y + displacementText);
});
}
}
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.radius + quad.point.radius + padding;
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;
});
};
}
},
update: function(data) {
methods.el = this;
var selector = methods.el;
//console.log("new data", data);
methods.animateBubbles(selector, data);
},
animateBubbles: function(selector, data) {
},
oldData: ""
};
var el = $this[0];
var options = {
data: data,
width: $(el).data("width"),
height: $(el).data("height")
}
if (data) {
methods.init(el, options);
}
});
</script>
<style>
.bubblechart {
text-align: center;
font-size: 12px;
}
.bubblechart .legend .label_group text,
.bubblechart .labelbubble text {
fill: #ffffff;
}
.bubblechart .labelbubble text {
font-size: 15px;
}
.bubblechart .labelbubble text.largetxt {
font-size: 25px;
}
@media screen and (max-width: 501px) {
.bubblechart .chart {
width: 100%;
height: 100%;
}
}
</style>
这是我拥有的 v4,但我无法重现 v3 的功能:
var $this = $('.bubblechart');
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
var width = $this.data('width'),
height = $this.data('height');
var color = d3.scaleOrdinal()
.range(["#ff5200", "red", "green"]);
var margin = {
top: 20,
right: 15,
bottom: 30,
left: 20
},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var svg = d3.select($this[0])
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr('class', 'bubblechart')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bubbles = svg.append('g').attr('class', 'bubbles');
var force = d3.forceSimulation()
.force("collide", d3.forceCollide(12))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(data);
var bubbles = svg.append("g")
.attr("class", "bubbles")
data = funnelData(data, width, height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 10)
.style("fill", function(d, i) {
return color(i);
})
.call(d3.drag());
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
draw('all');
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = width / 2;
elem.y = height / 2;
});
return data;
}
function draw(varname) {
var foci = {
"all": {
name: "All",
x: width / 2,
y: height / 2
}
};
force.on("tick", tick(foci, varname, .55));
}
function tick(foci, varname, k) {
return function(e) {
bubbles.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
}
body {
background: #eeeeee;
}
.line {
fill: none;
stroke-width: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>BubbleChart I</h1>
<div class="bubblechart" data-width="300" data-height="300" />
there used to be so much more features like gravity/charge and collision detection for these kind of things in d3v3
D3v4+ 在强制布局方面比 d3v3 具有更好的功能。但是,具有讽刺意味的是,查看您共享的 fiddle,您实际上并没有使用力布局来实际放置您的节点。尝试将重力或电荷更改为截然不同的值 - 没有任何变化,您永远不会使用 force.nodes(data)
.
将节点传递给模拟
也就是说,力模拟正在提供一个 alpha 值,重复调用 tick 函数等,但是节点和力模拟之间没有直接交互。所有定位都是在 tick 函数中手动完成的。力可以用计时器代替,以获得相同的结果。
这表明您问错了问题,而不是
- 如何将布局升级到 v4
很有可能:
- 如何将我的自定义定位功能合并到 d3v4 的强制布局中。
首先,让我们看看你的两个定位力:
- fiddle中的碰撞检测内置于d3v4(d3.forceCollide)中,代码与您使用的非常相似。
- 多焦点力定向图的完成方式与 d3v3 中的类似。下面我使用一个带有 forceX 和 forceY 的焦点,它与你的 fiddle.
相匹配
这里有一个简化的例子,在这里使用起来更容易:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var force = d3.layout.force()
var foci = {x: 150, y:150}
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
.map(function(d) { d.foci = foci; return d; })
force.start();
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
data.forEach(cluster);
data.forEach(collide(0.1));
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
function cluster(d,i) {
var f = d.foci;
var k = 0.55;
d.y += (f.y - d.y) * k * force.alpha()
d.x += (f.x - d.x) * k * force.alpha()
}
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.radius + quad.point.radius + padding;
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;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
如果删除 tick 函数中修改数据和转换的部分,您会看到节点保持完全静止。
我们可以很容易地将其更改为 d3v4+(好吧,d3-quad 需要第二次更新,否则,所有更改都是命名空间更改,例如:d3.scale.pow ⟶ d3.scalePow)。这让我们仍然利用您的定位功能并仅使用力布局来触发刻度、改变 alpha 等:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var force = d3.forceSimulation();
var foci = {x: 150, y:150}
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
.map(function(d) { d.foci = foci; return d; })
force.nodes(data);
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
data.forEach(cluster);
data.forEach(collide(0.1));
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
function cluster(d,i) {
var f = d.foci;
var k = 0.55;
d.y += (f.y - d.y) * k * force.alpha()
d.x += (f.x - d.x) * k * force.alpha()
}
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
function collide(alpha) {
var quadtree = d3.quadtree().x(function(d) { return d.x; }).y(function(d) { return d.y; }).extent([[-1, -1], [width + 1, height + 1]]).addAll(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.length && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.data.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
但是我们不要这样做,我们可以在tick函数中使用与之前相同的碰撞代码,但我们可以简化它。要使用 d3-forceSimulation 进行碰撞,我们可以使用 d3.forceCollide()
。我们要为其指定一个半径并修改碰撞力的强度(这是您代码段中的参数 alpha)。这可以按如下方式完成:
d3.forceCollide().radius(function(d) { return d.radius + padding; })
.strength(0.1)
由于您只有一个焦点,我将使用 d3 的 forceX 和 forceY 将节点推向该点。 fiddle中手动居中的等价物是:
.force("x", d3.forceX().x(width/2)
.strength(0.55))
.force("y", d3.forceY().y(height/2)
.strength(0.55))
总而言之:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
var force = d3.forceSimulation()
.force("collide", d3.forceCollide()
.radius(function(d) { return d.radius + padding + maxRadius; })
.strength(0.1))
.force("x", d3.forceX().x(width/2)
.strength(0.55))
.force("y", d3.forceY().y(height/2)
.strength(0.55))
.alpha(0.1) // same as v3.
force.nodes(data);
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
// Tinkered a bit here, did not dive into why a clearly identical solution was not immediately apparent.
var alpha = this.alpha();
this.force("collide").strength(0.2*alpha)
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
比较(时间差异只是我的滞后 - 我把顺序搞混了,他们不遵循上面的顺序):
我有一个 converted d3v4 气泡图,但在 d3v3 中曾经有更多的功能,例如 gravity/charge 和碰撞检测等。当图表加载时 - 我想看到 consistent/movement 个气泡,就像看池塘里的青蛙产卵一样 - 它们通过重力彼此靠近 - 但具有 repellant/charge 类型属性。泡泡也要尽量远离边缘
Here's 我在 v3 中寻找什么:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<div class="bubblechart" data-role="bubblechart" data-width="300" data-height="300" data="">
</div>
<script>
$(document).ready(function() {
var $this = $('.bubblechart');
//console.log("rendered div now engage d3", $this);
// set el height and width etc.
var w = $this.data("width");
var h = $this.data("height");
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
function colores_google(n) {
var colores_g = ["#ff7276", "#4baad2", "#eaa2a5", "#e75763", "#a6a19e"];
return colores_g[n % colores_g.length];
}
var methods = {
el: "",
init: function(el, options) {
var clone = options["data"];
var that = this;
//console.log("clone", clone);
w = options["width"];
h = options["height"];
methods.el = el;
methods.setup(clone, w, h);
//methods.resizeChart(methods.el["selector"]);
},
resizeChart: function(selector) {
//alert(selector);
var svg = $(selector + " .bubblechart");
var aspect = svg.width() / svg.height();
var targetWidth = svg.parent().parent().width();
if (targetWidth != null) {
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
}
},
funnelData: function(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
},
getMargin: function() {
return {
top: 30,
right: 25,
bottom: 50,
left: 25
};
},
setup: function(data, w, h) {
methods.width = w;
methods.height = h;
methods.fill = d3.scale.ordinal()
.range(["#d84b2a", "#beccae", "#7aa25c", "#008000"])
var margin = methods.getMargin();
var selector = methods.el;
var padding = 50;
/*
var svg = d3.select(selector)
.append("svg")
.attr("class", "bubblechart")
.attr("width", parseInt(w + margin.left + margin.right,10))
.attr("height", parseInt(h + margin.top + margin.bottom,10))
.attr('viewBox', "0 0 "+parseInt(w + margin.left + margin.right,10)+" "+parseInt(h + margin.top + margin.bottom,10))
.attr('perserveAspectRatio', "xMinYMid")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
*/
var chart = d3.select(selector).append("svg:svg")
.attr("class", "chart")
.attr("width", w - (w / 5))
.attr("height", h)
.attr("preserveAspectRatio", "none")
.attr("viewBox", "0 0 " + (w - (w / 5)) + " " + h + "")
.append("svg:g")
.attr("class", "bubblechart")
.attr("transform", "translate(-10,0)");
methods.force = d3.layout.force()
.charge(100)
.gravity(1200)
.size([methods.width, methods.height])
var bubbleholder = chart.append("g")
.attr("class", "bubbleholder")
var bubbles = bubbleholder.append("g")
.attr("class", "bubbles")
var labelbubble = bubbleholder.append("g")
.attr("class", "labelbubble")
//add legend
var legendPaddingTop = 30;
var legend = d3.select($this[0]).append("svg:svg")
.attr("class", "legend")
.attr("width", w / 5)
.attr("height", h)
.append("svg:g")
.attr("class", "legendsection")
.attr("transform", "translate(" + ((w / 4) + padding) + "," + legendPaddingTop + ")");
var label_group = legend.append("svg:g")
.attr("class", "label_group")
.attr("transform", "translate(" + (-(w / 3) + 20) + "," + 0 + ")");
var legend_group = legend.append("svg:g")
.attr("class", "legend_group")
.attr("transform", "translate(" + (-(w / 3) - 100) + "," + 0 + ")");
//draw labels
var labels = label_group.selectAll("text.labels")
.data(data);
var legendHeight = legendPaddingTop;
var ySpace = 18;
var labelPadding = 3;
labels.enter().append("svg:text")
.attr("class", "labels")
.attr("dy", function(d, i) {
legendHeight += ySpace;
return (ySpace * i) + labelPadding;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
labels.exit().remove();
var legend = legend_group.selectAll("circle").data(data);
legend.enter().append("svg:circle")
.attr("cx", 100)
.attr("cy", function(d, i) {
return ySpace * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
//reset legend height
//console.log("optimum height for legend", legendHeight);
$this.find('.legend').attr("height", legendHeight);
//add data
data = this.funnelData(data, methods.width, methods.height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (methods.width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.style("fill", function(d, i) {
return colores_google(i);
})
.call(methods.force.drag);
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
var bubblelabels = labelbubble.selectAll("text")
.data(data);
// Enter
bubblelabels.enter()
.append("text")
.attr("class", function(d) {
var cls = "title";
if (d.count > 9) {
cls += " largetxt";
}
return cls;
})
.text(function(d) {
return d.count;
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return (d.y) + 5;
});
// Update
bubblelabels
.transition()
.delay(300)
.duration(1000)
// Exit
bubblelabels.exit()
.transition()
.duration(250)
.remove();
draw('all');
function draw(varname) {
var foci = {
"all": {
name: "All",
x: methods.width / 2,
y: methods.height / 2
}
};
methods.force.on("tick", tick(foci, varname, .55));
methods.force.start();
}
function tick(foci, varname, k) {
return function(e) {
data.forEach(function(o, i) {
var f = foci[o[varname]];
o.y += (f.y - o.y) * k * e.alpha;
o.x += (f.x - o.x) * k * e.alpha;
});
nodes
.each(collide(.1))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
bubblelabels
.each(collide(.1))
.attr("x", function(d) {
var displacementText = -5;
if (d.count > 9) {
displacementText = -14;
}
return (d.x + displacementText);
})
.attr("y", function(d) {
var displacementText = 5;
if (d.count > 9) {
displacementText = 7;
}
return (d.y + displacementText);
});
}
}
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.radius + quad.point.radius + padding;
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;
});
};
}
},
update: function(data) {
methods.el = this;
var selector = methods.el;
//console.log("new data", data);
methods.animateBubbles(selector, data);
},
animateBubbles: function(selector, data) {
},
oldData: ""
};
var el = $this[0];
var options = {
data: data,
width: $(el).data("width"),
height: $(el).data("height")
}
if (data) {
methods.init(el, options);
}
});
</script>
<style>
.bubblechart {
text-align: center;
font-size: 12px;
}
.bubblechart .legend .label_group text,
.bubblechart .labelbubble text {
fill: #ffffff;
}
.bubblechart .labelbubble text {
font-size: 15px;
}
.bubblechart .labelbubble text.largetxt {
font-size: 25px;
}
@media screen and (max-width: 501px) {
.bubblechart .chart {
width: 100%;
height: 100%;
}
}
</style>
这是我拥有的 v4,但我无法重现 v3 的功能:
var $this = $('.bubblechart');
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
var width = $this.data('width'),
height = $this.data('height');
var color = d3.scaleOrdinal()
.range(["#ff5200", "red", "green"]);
var margin = {
top: 20,
right: 15,
bottom: 30,
left: 20
},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var svg = d3.select($this[0])
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr('class', 'bubblechart')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bubbles = svg.append('g').attr('class', 'bubbles');
var force = d3.forceSimulation()
.force("collide", d3.forceCollide(12))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(data);
var bubbles = svg.append("g")
.attr("class", "bubbles")
data = funnelData(data, width, height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 10)
.style("fill", function(d, i) {
return color(i);
})
.call(d3.drag());
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
draw('all');
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = width / 2;
elem.y = height / 2;
});
return data;
}
function draw(varname) {
var foci = {
"all": {
name: "All",
x: width / 2,
y: height / 2
}
};
force.on("tick", tick(foci, varname, .55));
}
function tick(foci, varname, k) {
return function(e) {
bubbles.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
}
body {
background: #eeeeee;
}
.line {
fill: none;
stroke-width: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>BubbleChart I</h1>
<div class="bubblechart" data-width="300" data-height="300" />
there used to be so much more features like gravity/charge and collision detection for these kind of things in d3v3
D3v4+ 在强制布局方面比 d3v3 具有更好的功能。但是,具有讽刺意味的是,查看您共享的 fiddle,您实际上并没有使用力布局来实际放置您的节点。尝试将重力或电荷更改为截然不同的值 - 没有任何变化,您永远不会使用 force.nodes(data)
.
也就是说,力模拟正在提供一个 alpha 值,重复调用 tick 函数等,但是节点和力模拟之间没有直接交互。所有定位都是在 tick 函数中手动完成的。力可以用计时器代替,以获得相同的结果。
这表明您问错了问题,而不是
- 如何将布局升级到 v4
很有可能:
- 如何将我的自定义定位功能合并到 d3v4 的强制布局中。
首先,让我们看看你的两个定位力:
- fiddle中的碰撞检测内置于d3v4(d3.forceCollide)中,代码与您使用的非常相似。
- 多焦点力定向图的完成方式与 d3v3 中的类似。下面我使用一个带有 forceX 和 forceY 的焦点,它与你的 fiddle. 相匹配
这里有一个简化的例子,在这里使用起来更容易:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var force = d3.layout.force()
var foci = {x: 150, y:150}
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
.map(function(d) { d.foci = foci; return d; })
force.start();
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
data.forEach(cluster);
data.forEach(collide(0.1));
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
function cluster(d,i) {
var f = d.foci;
var k = 0.55;
d.y += (f.y - d.y) * k * force.alpha()
d.x += (f.x - d.x) * k * force.alpha()
}
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.radius + quad.point.radius + padding;
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;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
如果删除 tick 函数中修改数据和转换的部分,您会看到节点保持完全静止。
我们可以很容易地将其更改为 d3v4+(好吧,d3-quad 需要第二次更新,否则,所有更改都是命名空间更改,例如:d3.scale.pow ⟶ d3.scalePow)。这让我们仍然利用您的定位功能并仅使用力布局来触发刻度、改变 alpha 等:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var force = d3.forceSimulation();
var foci = {x: 150, y:150}
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
.map(function(d) { d.foci = foci; return d; })
force.nodes(data);
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
data.forEach(cluster);
data.forEach(collide(0.1));
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
function cluster(d,i) {
var f = d.foci;
var k = 0.55;
d.y += (f.y - d.y) * k * force.alpha()
d.x += (f.x - d.x) * k * force.alpha()
}
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
function collide(alpha) {
var quadtree = d3.quadtree().x(function(d) { return d.x; }).y(function(d) { return d.y; }).extent([[-1, -1], [width + 1, height + 1]]).addAll(data);
return function(d) {
var r = d.radius + maxRadius + padding,
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.length && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.data.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
但是我们不要这样做,我们可以在tick函数中使用与之前相同的碰撞代码,但我们可以简化它。要使用 d3-forceSimulation 进行碰撞,我们可以使用 d3.forceCollide()
。我们要为其指定一个半径并修改碰撞力的强度(这是您代码段中的参数 alpha)。这可以按如下方式完成:
d3.forceCollide().radius(function(d) { return d.radius + padding; })
.strength(0.1)
由于您只有一个焦点,我将使用 d3 的 forceX 和 forceY 将节点推向该点。 fiddle中手动居中的等价物是:
.force("x", d3.forceX().x(width/2)
.strength(0.55))
.force("y", d3.forceY().y(height/2)
.strength(0.55))
总而言之:
var width = 300,
height = 300,
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var data = funnelData([{"label": "Chinese","value": 20}, {"label": "American","value": 10}, {"label": "Indian","value": 50}],width,height)
var maxRadius = d3.max(data, function(d) { return d.radius; })
var padding = 4;
var force = d3.forceSimulation()
.force("collide", d3.forceCollide()
.radius(function(d) { return d.radius + padding + maxRadius; })
.strength(0.1))
.force("x", d3.forceX().x(width/2)
.strength(0.55))
.force("y", d3.forceY().y(height/2)
.strength(0.55))
.alpha(0.1) // same as v3.
force.nodes(data);
var node = svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', 1)
.attr('fill', function (d,i) {
return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
})
node.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) { return d.value * (width / 6) / 100; })
force.on("tick", function () {
// Tinkered a bit here, did not dive into why a clearly identical solution was not immediately apparent.
var alpha = this.alpha();
this.force("collide").strength(0.2*alpha)
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
});
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
data.forEach(function(elem, index) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
比较(时间差异只是我的滞后 - 我把顺序搞混了,他们不遵循上面的顺序):