背景介绍
由于项目大屏可视化的需求,需要实现在一个不定长宽的长方形容器中,实现不重叠分布的气泡图,每个气泡代表一类数据的统计值,气泡个数最大值已知,气泡大小与数据值大小正相关,并且气泡图需要有浮动特效,页面尺寸改变时,气泡尺寸需要自适应容器尺寸。
方案选择
方案一:echarts实现
使用echarts的关系图graph来实现,使用参数layout: force采用力引导布局,官网对于力引导布局的解释是:力引导布局是模拟弹簧电荷模型在每两个节点之间添加一个斥力,每条边的两个节点之间添加一个引力,每次迭代节点会在各个斥力和引力的作用下移动位置,多次迭代后节点会静止在一个受力平衡的位置,达到整个模型的能量最小化。因此通过一定的规则可以得到气泡的最大直径,设置参数force.repulsion可以实现气泡相互排斥不重叠。
实现步骤:
1、获取容器的长度和宽度。
2、通过数据值的大小计算气泡的半径。这里是设置一个最小半径rMin和最大半径rMax,最小半径对应数据的最小值,最大半径对应数据的最大值,以此为依据计算其余气泡的半径:
const delta = (rMax - rMin) / (valueMax - valueMin)
node.r = (node.value - valueMin) * (1 + delta)
3、计算斥力值大小为repulsion = rMax * 2
4、组装echarts参数并初始化echarts
option = {
backgroundColor: '#fff',
animationEasingUpdate: 'bounceIn',
series: [{
type: 'graph',
layout: 'force',
force: {
repulsion: repulsion
},
// 是否开启鼠标缩放和平移漫游
roam: false,
label: {normal: {show: true}},
data: nodes
}]
}
最终效果图如下:
优点:计算部分较为简单,echarts作为一个成熟的大众化的图标工具,使用较为常见。
缺点:没有边界检测,当气泡个数较多时无法精确计算气泡的最大半径,容易超出容器。而且无法给graph添加缓动效果。
适用场景:气泡个数较少且不需要复杂动画的场景。
方案二:圆包裹算法计算每个气泡的大小和位置,通过添加页面元素实现。
Circle packing问题:如何在一个平面或球面上,尽可能地放置一些圆,使得它们不重叠,不留空隙。这个问题正好符合Circle packing的求解,现已存在很多成熟的circle packing求解算法。采用d3.js中就有此类模型,通过阅读解析d3.js的源码,抽取出关键函数打包,调用后生成每个气泡的x, y, r后直接生成dom,为每个dom添加上下浮动css动画。
算法关键函数:
/* 根据指定的层次结构数据构造一个根节点root */
function hierarchy(data, children) {
console.log(data, 'data', children, 'children')
var root = new Node(data),
valued = +data.value && (root.value = data.value),
node,
nodes = [root],
child,
childs,
n;
console.log(root, 'root', nodes, 'nodes')
if (children == null) children = defaultChildren;
while (node = nodes.pop()) {
if (valued) node.value = +node.data.value;
if ((childs = children(node.data)) && (n = childs.length)) {
node.children = new Array(n);
for (i = n - 1; i >= 0; --i) {
nodes.push(child = node.children[i] = new Node(childs[i]));
child.parent = node;
child.depth = node.depth + 1;
}
}
}
return root.eachBefore(computeHeight);
}
/* 对 root hierarchy 进行布局,root 节点以及每个后代节点会被附加以下属性:
node.x - 节点中心的 x- 坐标
node.y - 节点中心的 y- 坐标
node.r - 圆的半径
在传入布局之前必须调用 root.sum。可能还需要调用 root.sort 对节点进行排序。 */
function setPack() {
var radius = null,
dx = 1,
dy = 1,
padding = constantZero;
function pack(root) {
root.x = dx / 2, root.y = dy / 2;
if (radius) {
root.eachBefore(radiusLeaf(radius))
.eachAfter(packChildren(padding, 0.5))
.eachBefore(translateChild(Math.min(dx, dy) / (2 * root.r)));
// .eachBefore(translateChild(1));
} else {
root.eachBefore(radiusLeaf(defaultRadius$1))
.eachAfter(packChildren(constantZero, 1))
.eachAfter(packChildren(padding, root.r / Math.min(dx, dy)))
.eachBefore(translateChild(Math.min(dx, dy) / (2 * root.r)));
}
return root;
}
pack.radius = function(x) {
return arguments.length ? (radius = optional(x), pack) : radius;
};
pack.size = function(x) {
return arguments.length ? (dx = +x[0], dy = +x[1], pack) : [dx, dy];
};
pack.padding = function(x) {
return arguments.length ? (padding = typeof x === "function" ? x :
constant$8(+x), pack) : padding;
};
return pack;
};
/* 计算能包裹一组 circles 的 smallest circle(最小圆),
每个圆必须包含 circle.r 属性表示半径,
以及 circle.x 以及 circle.y 属性表示圆的中心 */
function packEnclose(circles) {
if (!(n = circles.length)) return 0;
var a, b, c, n, aa, ca, i, j, k, sj, sk;
// Place the first circle.
a = circles[0], a.x = 0, a.y = 0;
if (!(n > 1)) return a.r;
// Place the second circle.
b = circles[1], a.x = -b.r, b.x = a.r, b.y = 0;
if (!(n > 2)) return a.r + b.r;
// Place the third circle.
place(b, a, c = circles[2]);
// Initialize the front-chain using the first three circles a, b and c.
a = new Node$1(a), b = new Node$1(b), c = new Node$1(c);
a.next = c.previous = b;
b.next = a.previous = c;
c.next = b.previous = a;
// Attempt to place each remaining circle…
pack: for (i = 3; i < n; ++i) {
place(a._, b._, c = circles[i]), c = new Node$1(c);
// Find the closest intersecting circle on the front-chain, if any.
// “Closeness” is determined by linear distance along the front-chain.
// “Ahead” or “behind” is likewise determined by linear distance.
j = b.next, k = a.previous, sj = b._.r, sk = a._.r;
do {
if (sj <= sk) {
if (intersects(j._, c._)) {
b = j, a.next = b, b.previous = a, --i;
continue pack;
}
sj += j._.r, j = j.next;
} else {
if (intersects(k._, c._)) {
a = k, a.next = b, b.previous = a, --i;
continue pack;
}
sk += k._.r, k = k.previous;
}
} while (j !== k.next);
// Success! Insert the new circle c between a and b.
c.previous = a, c.next = b, a.next = b.previous = b = c;
// Compute the new closest circle pair to the centroid.
aa = score(a);
while ((c = c.next) !== b) {
if ((ca = score(c)) < aa) {
a = c, aa = ca;
}
}
b = a.next;
}
// Compute the enclosing circle of the front chain.
a = [b._], c = b; while ((c = c.next) !== b) a.push(c._); c = enclose(a);
// Translate the circles to put the enclosing circle around the origin.
for (i = 0; i < n; ++i) a = circles[i], a.x -= c.x, a.y -= c.y;
return c.r;
}
html与css特效:
最终效果图如下:
优点:不管有多少个气泡,都可以实现,且可以自己实现各种特效和动画。
缺点:算法复杂,代码量较大。
refs: 不重叠且分布均匀的气泡图解决方案(基于echarts)_echarts气泡图_河鲜森的博客-CSDN博客
echarts实现气泡图(气泡之间不叠加)_echarts气泡图_之毅的博客-CSDN博客