前端不规则气泡图实现方案

背景介绍

由于项目大屏可视化的需求,需要实现在一个不定长宽的长方形容器中,实现不重叠分布的气泡图,每个气泡代表一类数据的统计值,气泡个数最大值已知,气泡大小与数据值大小正相关,并且气泡图需要有浮动特效,页面尺寸改变时,气泡尺寸需要自适应容器尺寸。

方案选择

方案一: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博客

1.16. 层布局(树、集群、矩阵、打包、分区) · D3-V4.js API 中文手册 · 看云

https://github.com/xswei/d3-hierarchy#pack

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值