CCS+JS绘制星型拓扑图(关系图)

1. 需求

业务上,有时候需要展示与某一特定目标关联的其它相关元素,可能会用关系图来表示(专业名称为星型网络拓扑图),如下:

一般来说,像d3.jsEcharts 这些库都会有相应的实现,可以直接引用它们的API来实现。

但是,如果工程中只有这一处使用这种图表,不想因为这一个需求就引入一整个库,或者不想承受这些库所带来的性能开销(canvas绘图非常耗性能),那可以考虑本文将要介绍的这种使用纯CSS+原生JS 的方案尝试一下。

2. 需求分析

对于整个需求,我们可以分为两部分:

  • 中间的目标元素

    • 它由内外两层圈组成
    • 外圈带有渐变,内层是虚线边框
    • 外圈逆时针旋转, 外层逆时针旋转
  • 周围的关联元素
    • 与目标元素之间有连线
    • 均匀分布在目标元素周围

下面就分步骤来分别实现这两个需求

3. 目标元素

环形渐变

一开始,能想到的方案是外层一个正方形,然后设置一定宽度的边框,边框border-image 属性设置为一个渐变色,但是经过尝试,边框的渐变色无法达到预期的均匀分布的效果,故放弃,采取外圆-内圆=环形 的方案。

  1. 首先我们有一个外层盒子
  2. 然后里面有一个内层盒子,内层盒子比外层盒子稍小
  3. 然后外层盒子设置一个背景色并利用圆角边框属性裁剪为圆形,
  4. 内层盒子设置背景色为白色并利用圆角边框属性裁剪为圆形
  5. 通过定位将内层盒子放在合适的位置,形成一个环,代码与效果如下:
<div class="outer-box"><div class="inner-box"></div>
</div><style>.outer-box {width: 200px;height:200px;position: relative;border-radius: 50% 50%;background: blue;}.inner-box {width: 180px;height: 180px;position: absolute;left:10px;top:10px;background-color: #fff;border-radius: 50% 50%;}</style>

效果如下:

外部圆环就出来了。

这里的关键点是

但是这个圆环要是一个均匀分布的渐变色, 即使设置外层盒子background-image属性为一个渐变色,也达不到需求所要的效果,怎么办呢?

仔细观察需求,其实是一组渐变色在圆环上顺着时针方向重复了四次。那假设组成这组渐变的四个颜色点分别是#1,#2,#3,#4,它们在圆环上其实是像下面这样排列的:

既然这样,我们完全可以把它们分成四份,每一份的背景色都是由 #1、#2、#3、#4组成的渐变,只不过渐变的方向不同而已。那我们就在外层盒子中再放入四个小盒子,每个盒子大小占外层盒子的四分之一,然后设置同样的渐变色不同的渐变方向:

<div class="outer-box"><div class="left-top"></div><div class="right-top"></div><div class="left-bottom"></div><div class="right-bottom"></div><div class="inner-box"></div>
</div><style>.left-top, .right-top, .left-bottom, .right-bottom {width: 100px;height: 100px;position: absolute;}.left-top {background: linear-gradient(to top right, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);top:0;left:0;}.right-top {background: linear-gradient(to bottom right, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);top:0;left:100px;}.left-bottom {background: linear-gradient(to left top, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);left:0;top:100px;}.right-bottom {background: linear-gradient(to left bottom, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);left: 100px;top: 100px;}
</style>

可以看到效果如下:

可是,由于border-radius 只作用于边框,边框变性后并不影响内容的展示,所以我们发现四个小盒子其实没有形成圆环,我们有两种方案解决这个问题:

方案一:利用overflow属性

.outer-box {width: 200px;height:200px;position: relative;border-radius: 50% 50%background: blue;ovarflow: hidden;
}

方案二: 利用clip-path,可以不再使用圆角边框

.outer-box {width: 200px;height:200px;position: relative;clip-path: circle(50% at 50% 50%); background: blue;}

完成后效果如下:

可以看到,外层圆环基本成型。再给它加上小圆点,就完全符合需求了:

<div class="outer-box"><div class="left-top"><div class="litle-circle"></div></div><div class="right-top"><div class="litle-circle"></div></div><div class="left-bottom"><div class="litle-circle"></div></div><div class="right-bottom"><div class="litle-circle"></div></div><div class="inner-box"></div>
</div><style>.litle-circle {width:10px;height: 10px;background-color: #4094FF;clip-path: circle(50% at 50% 50%);}.left-top .litle-circle {margin-top:90px;margin-left: 0;}.right-top .litle-circle {margin-top: 0;margin-left: 0;}.left-bottom .litle-circle {margin-top:90px;margin-left: 90px;}.right-bottom .litle-circle {margin-left: 90px;margin-top: 0;}
</style>

最终环形渐变如下:

然后让它动起来,就是添加一个动画效果:

.outer-box {width: 200px;height:200px;position: relative;clip-path: circle(50% at 50% 50%);background: blue;animation: spin 6s linear infinite;
}@keyframes spin{to {transform: rotate(-1turn);}
}

其中,-1turn 表示逆时针旋转一周。

效果如下

内部圈

然后添加内部转动的圈,这个内部圈可以加到内部盒子里:

<div class="inner-box"><div class="inner-rotate-box"></div>
</div><style>.inner-rotate-box {width: 150px;height: 150px;left:15px;top: 15px;position:absolute;border:4px dashed blue;border-radius: 50% 50%;box-sizing: border-box;}
</style>

主要是利用border:4px dashed blue 给它设置一个虚线边框

效果:

我们发现内层是跟着外层转动的,而需求时内层要与外层相反,顺时针旋转,我们通过设置动画参数来让它反向旋转:

.inner-rotate-box {animation: spin 12s linear infinite;animation-direction: reverse;
}

我们发现,其实没有效果,那是因为它的外层盒子inner-box 还是跟着outer-box一起旋转的,我们给它的外层盒子也加上反向旋转:

.inner-box {animation: inherit;animation-direction: reverse;
}

通过继承outer-box的动画并进行反向运动,可以抵消outer-box带给inner-box的旋转动画,这时看起来inner-box就像是没有在旋转一样,而它内部的旋转盒子又是与外层盒子反向旋转的,就能实现内外相反运动的效果:

接着,我们再给内层旋转的圆添加一层点状虚线圈:


<div class="inner-box"><div class="inner-rotate-box"></div><div class="inner-dotted-box"></div>
</div><style>.inner-dotted-box {width: 154px;height:154px;position:absolute;top:13px;left:13px;border: 2px dotted #B5D5FF;border-radius: 50% 50%;box-sizing: border-box;}
</style>

这里关键点有两个:

  • 这里的虚线是点状虚线,所有边框样式用dotted;
  • 这里和上面的inner-rotate-box 都要设置box-sizingborder-box, 以便使我们的位置计算更为精确;

效果如下:

最后,我们把内容添加上,注意,内容盒子要和旋转盒子和点状虚线盒子同级,直接放在内层盒子里,原因是我们上面说过的,内层盒子inner-box通过与外层盒子反向旋转抵消了旋转,而看起来就像是没有旋转一样,内容只有放在它里面,才不会跟着旋转。

<div class="inner-box"><div class="inner-rotate-box"></div><div class="inner-dotted-box"></div><div class="inner-content">目标内容</div>
</div><style>.inner-content {width: 120px;height: 120px;position: absolute;top: 30px;left: 30px;display: flex;justify-content: center;align-items: center;}</style>

效果如下:

下面是整个目标元素的实现代码,可供参考:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>双层圆环反向圆周运动示例</title><style>.outer-box {width: 200px;height:200px;position: relative;clip-path: circle(50% at 50% 50%);border-radius: 50% 50%;background: blue;animation: spin 6s linear infinite;}.inner-box {width: 180px;height: 180px;position: absolute;left:10px;top:10px;background-color: #fff;border-radius: 50% 50%;animation: inherit;animation-direction: reverse;}.left-top, .right-top, .left-bottom, .right-bottom {width: 100px;height: 100px;position: absolute;}.left-top {background: linear-gradient(to top right, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);top:0;left:0;}.right-top {background: linear-gradient(to bottom right, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);top:0;left:100px;}.left-bottom {background: linear-gradient(to left top, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);left:0;top:100px;}.right-bottom {background: linear-gradient(to left bottom, #C3DDFF 25%, #F1f7ff 100%, #78B3FF 75%, #98C5FF 100%);left: 100px;top: 100px;}.litle-circle {width:10px;height: 10px;background-color: #4094FF;clip-path: circle(50% at 50% 50%);}.left-top .litle-circle {margin-top:90px;margin-left: 0;}.right-top .litle-circle {margin-top: 0;margin-left: 0;}.left-bottom .litle-circle {margin-top:90px;margin-left: 90px;}.right-bottom .litle-circle {margin-left: 90px;margin-top: 0;}.inner-rotate-box {width: 150px;height: 150px;left:15px;top: 15px;position: absolute;border:4px dashed blue;border-radius: 50% 50%;box-sizing: border-box;animation: spin 12s linear infinite;animation-direction: reverse;}.inner-dotted-box {width: 154px;height:154px;position:absolute;top:13px;left:13px;border: 2px dotted #B5D5FF;border-radius: 50% 50%;box-sizing: border-box;}.inner-content {width: 120px;height: 120px;position: absolute;top: 30px;left: 30px;display: flex;justify-content: center;align-items: center;}@keyframes spin{to {transform: rotate(-1turn);}}</style>
</head>
<body><div class="outer-box"><div class="left-top"><div class="litle-circle"></div></div><div class="right-top"><div class="litle-circle"></div></div><div class="left-bottom"><div class="litle-circle"></div></div><div class="right-bottom"><div class="litle-circle"></div></div><div class="inner-box"><div class="inner-rotate-box"></div><div class="inner-dotted-box"></div><div class="inner-content">目标内容</div>        </div></div>
</body>
</html>

4. 关联元素

对于关联元素,则相对复杂一点,我们要考虑的是:

  • 关联元素个数不定,可能很少,也可能很多,所以少的时候就要大一点,多的时候就要小一点,这就需要计算关联元素的宽度,又由于都是圆形展示,所以都是一个正方形盒子裁切成圆形
  • 关联元素要围绕目标元素均匀分布
  • 关联元素与目标元素之间要有连线

基于以上几个点,我们设计了如下算法:

4.1 容器区块划分

我们把整个外层容器设计成一个正方形,然后使用水平平分线、垂直平分线和两条对角线将它划分为8个区块,加上这几条线本身切割形成的8条线也算一个区块,总共16个区块:

4.2 根据数量计算关联元素尺寸、弧度及角度

根据关联元素的数量,计算关联元素的尺寸、弧度和角度

即如果有N个关联元素,并且N> 2, 则我们认为可以将N个元素分别绘制到容器上半部分和下半部分,即将所有元素平分到两个数组里,如果是奇数个,则上半部分的数组里就多一个。

const rowLength = Math.ceil(this.unitRelations.length / 2); // 向上取整
const row1 = [];
const row2 = [];
for (let j = 0; j < this.unitRelations.length; j++) {j < rowLength ? row1.push(j) : row2.push(j);
}

然后计算它的尺寸,就是看元素所在那半部分一共有几个元素,然后设置一定的间隔距离,用整个容器的宽度减去它们之间的间隔的总和,再除以这半部分的元素个数,就获得了它的尺寸,当然,我们这里还需要设置一个最大尺寸,防止它的尺寸太大,在视觉上显得比目标元素还要重要。

const maxSize = 100;
const originalSize = this.graphWidth / rowLength;
const gap = originalSize / 10; // 间隔设置为元素无间隔平分区域时的大小的十分之一
const size = (this.graphWidth - gap * (rowLength - 1)) / rowLength;
const result = size > maxSize ? maxSize : size;

然后计算出每个元素平均的弧度和角度:

const baseRadian = parseInt(360 / this.unitRelations.length); // 每一份的基本弧度
const radian = 30 + baseRadian * i; // 获得旋转弧度,我们将起始点设置为30度
const minRadian = this.isInRow([1, 3, 5, 7], this.getBlockIndex(radian)) ? radian % 45 : 45 - radian % 45;
const angle = minRadian * Math.PI / 180; // 获得弧度相应的角度

这里,minRadian 其实是用来计算当前元素参与位置计算时它的实际弧度,由于我们要用直角三角形的三角函数来计算它的位置,所以它的基础弧度要小于45度,而它处在不同区块时,用来计算的弧度也是不一样的:

  • 如果它处于1/3/5/7区块,则参与计算的弧度就是它取模后的弧度
  • 如果它处于2/4/6/8区块,则参与计算的弧度是它的弧度取模后与45的插值

如图:

分别落在第3和第4区块内的两个元素,第3区块参与计算的弧度就是它弧度取模后的德尔塔1,而第4区块的元素参与计算的弧度实际上是取模后的弧度德尔塔2与区块弧度45之间的差值德尔塔3。

有了元素尺寸和参与计算的角度,我们又有容器的宽高, 根据上图,我们很容易就可计算出元素距容器左边和上边的距离,这样元素的位置就确定了。

具体计算过程需要用到三角函数,可以自行计算。

边框吸附

注意上图,我们之所以能够对元素位置进行计算,还用到了一个技巧,就是边框吸附,我们默认关联元素时吸附在外层容器边框上的,当然,根据其所处区块不同,其吸附的边有可能是上、下、左、右其中一边,

代码如下:

const { radian, opposite } = this.getBaseProperty(i);const block = this.getBlockIndex(radian);if (block === 1) {return {top: this.graphWidth / 2 - opposite - width / 2,left: 0};} else if (block === 2) {return {top: 0,left: this.graphWidth / 2 - opposite - width / 2};} else if (block === 3) {return {top: 0,left: this.graphWidth / 2 + opposite - width / 2};} else if (block === 4) {return {left: this.graphWidth - width,top: this.graphWidth / 2 - opposite - width / 2};} else if (block === 5) {return {left: this.graphWidth - width,top: this.graphWidth / 2 + opposite - width / 2};} else if (block === 6) {return {left: this.graphWidth / 2 + opposite - width / 2,top: this.graphWidth - width};} else if (block === 7) {return {left: this.graphWidth / 2 - opposite - width / 2,top: this.graphWidth - width};} else if (block === 8) {return {left: 0,top: this.graphWidth / 2 + opposite - width / 2};} else if (block === 1.5) {return {left: 0,top: 0};} else if (block === 2.5) {return {left: this.graphWidth / 2 - width / 2,top: 0};} else if (block === 3.5) {return {left: this.graphWidth - width / 2,top: 0};} else if (block === 4.5) {return {left: this.graphWidth - width,top: this.graphWidth / 2 - width / 2};} else if (block === 5.5) {return {left: this.graphWidth - width / 2,top: this.graphWidth - width};} else if (block === 6.5) {return {left: this.graphWidth / 2 - width / 2,top: this.graphWidth - width};} else if (block === 7.5) {return {left: 0,top: this.graphWidth - width};} else if (block === 8.5) {return {left: 0,top: this.graphWidth / 2 - width / 2};}

可以看到,不同的区块,其吸附的边不一样,具体表现就是直接设置其left为0或top为0,或者left为容器宽度减去元素尺寸或者容器高度(其实也就是容器宽度)减去元素尺寸等等;

连线

对于连线,我们设计为关联元素内部的一个绝对定位的元素:

 <li v-for="(site, index) in unitRelations" :key="index" :style="getStyle(index)"><div class="connector" :style="getConnectStyle(index)"><span class="one" :style="getDirect(index)"></span><span class="two" :style="getDirect(index)"></span><p>{{ site.unitType }}</p></div><div class="site-info"><p>{{ site.mediaName }} <br /> {{ site.siteName }}</p></div></li>

上面是Vue项目中应用的代码片段,其中类名为connect的元素就是连线。类名为onetwo的两个元素就是需求中连线上不断运动的小点,这个很好实现,就不讲了,主要讲下连线的长度和角度计算。

连线默认其实就是关联元素中一个绝对定位的长方形,它的固定高度可以设为2-5像素(根据需要)来模拟一根线,然后初始高度就是元素的高度的一半位置,而水平偏移量则根据其所在不同区块而不同:如图

然后我们计算它的宽度,由于我们已经确定了关联元素的位置和角度,就可以直接根据元素位置的left值和top值,结合容器的宽高,利用三角函数,计算出元素圆心到目标元素圆心的连线距离,如图所示:

计算出连线长度后,上文所说的连线初始水平偏移量也就是它的长度(或负的长度,根据偏移方向不同),

然后将它旋转和关联元素一样的弧度,就可以了。

当然,关联元素的大小、位置算法还很基础,有兴趣的同学可以尝试更好的算法,比如各个元素的尺寸可以增加一定弹性系数,呈现出有大有小,近大远小的视觉效果,位置也可以不用吸附,而是根据区块内元素拥挤程度,适当调节某些元素的位置,有的可以离目标近点,有的可以离目标远一点。

由于这部分算法还不算最优,就补贴具体代码了,读者可以根据以上思路和关键点自行进行开发和算法优化。

CCS+JS绘制星型拓扑图(关系图)相关推荐

  1. python绘制两个离散变量关系图——马赛克图

    一个比较好看的图如下: 我们可以较为直观的看到两个离散变量之间的关系,python绘制方法也比较简单 可以使用statsmodels.graphics.mosaicplot.mosaic 文档位置:h ...

  2. tableau高级绘图(三)-tableau绘制王者荣耀人物关系图

    最终效果图如上 成品及数据 一.数据准备 1. 收集所有英雄信息(王者荣耀游戏阵营信息收集),为防止数据过度密集,以阵营归属划分,rand随机数赋值坐标 2. 数据准备时,关于a-b的关系(王者人物阵 ...

  3. d3力导向图增加节点_D3.js+Es6+webpack构建人物关系图(力导向图),动态更新数据,点击增加节点,拖拽增加连线......

    Java学习手记2--多线程 一.线程的概念 CPU执行程序,就好比一个人在干事情一样,同一个时间你只能做一件事情,但是这样的效率实在是太低了,在你用电脑的时候,听歌就不能浏览网页,看电影就不能下载视 ...

  4. vue 拓扑组件_vue.js生成S型拓扑图

    1.前端代码 new Vue({ el: '#app', data: { }, mounted() { this.init() }, methods: { init() { axios.get(sit ...

  5. 如何做MySQL的星型结构_MySQL Sakila示例数据库的星型模型

    Sakila样本数据库介绍 Sakila样本数据库是MySQL官方提供的一个虚拟的DVD出租连锁店数据库,提供了一个标准模式.Sakila数据库支撑了DVD租赁商店的业务流程.你可以在这个地址下载到数 ...

  6. Echarts绘制关系图

    文章目录 基本应用 节点重名问题解决方案 两点之间如何绘制多条连线 Echarts图随着浏览器窗口的变化而变化 今天要发博客,因为今天再不发,2020就过去啦! 最近需要用到Echarts绘制关系图, ...

  7. 编程随想 关系图_IT什么岗位比较好找工作?一张金字塔图就能明白

    IT(Internet Technology)互联网技术是指在计算机技术的基础上开发建立的一种信息技术.IT行业这些年一直很火爆, 对于IT就业岗位的选择一直也都是热门话题. 一.IT人才总体供需 金 ...

  8. qam映射c程序_基于星型24QAM映射的光概率成型编码方法与流程

    本发明涉及一种光概率成型编码方法,特别是一种基于星型24QAM映射的光概率成型编码方法. 背景技术: 接入网是指用户终端与主干网络之间的所有设备,长度从几百米到几公里不等,因而常常被称为"最 ...

  9. 企业级数据仓库:数据仓库概述;核心技术框架,数仓理论,数据通道Hive技术框架,HBase设计,系统调度,关系模式范式,ER图,维度建模,星型/雪花/星座模式,数据采集同步,业务数据埋点,数据仓库规范

    文章目录 第一章 数据仓库概述 1.1 数据仓库简介 1.1.2 什么是数据仓库? 1.1.3 OLTP 与 OLAP 1.2 数据仓库技术架构 1.3 课程目标 第二章 核心技术框架 2.1 数据仓 ...

  10. Vue 使用 vis-network 绘制网络关系图

    1.Vis-network visjs 提供了一个网络视图模块,提供给我们绘制网络之间的各个点.线之间的关系,这个的话就比较类似于echarts的地图,在地图上打点画线的逻辑,区别在于使用visjs可 ...

最新文章

  1. python闭包主要解决什么问题_关于python中闭包的总结
  2. 遇见那个对的人,便是爱情
  3. 毕业课题之------------图像的形态学滤波
  4. 【Spring注解系列02】@CompentScan与@CompentScans
  5. 20140904 atoi字符串转化为整数源码
  6. SQL基础整理——例题
  7. 【ES6(2015)】Symbol
  8. select时尽可能少使用as对性能很有好处
  9. React Native For Android 架构初探
  10. dw php重复区域横向,php横向重复区域显示二法
  11. 全网首发:无线网桥的延迟太大,有时达到10秒以上
  12. 西门子PCS7常见报警及故障说明
  13. Assimp库中文文档
  14. SQLyog学习笔记04---数据库表操作CRUD基本指令
  15. 个人征信要良好,申请信用卡需注意哪些事项?
  16. 区块链十年一梦:有人辞官归故里,有人星夜来赶考
  17. 59 Three.js 渲染两个场景和使用不同的相机,渲染在一个场景里面
  18. 努力前端【LeetCode-10】448. 找到所有数组中消失的数字 442. 数组中重复的数据(中等) 41. 缺失的第一个正数(困难) [鸽笼原理,数组,Map,类似No.645]
  19. supermap使用idesktop发布二三维管线地图
  20. 沪胶809合约交割日近,压制远期合约走弱

热门文章

  1. udacity 学java_刷完udacity的JavaScript,我想说……
  2. 你的才艺怎样变现?--Rarible平台
  3. springboot2.x 整合 elasticsearch 创建索引的方式
  4. 第一篇博客--大学成长指南
  5. 苹果处理器性能排行榜天梯图2022 苹果处理器排行榜2022
  6. sklearn的系统学习——决策树分类器(含有python完整代码)
  7. 重学计算机组成原理(五)- 旋转跳跃的指令实现
  8. 数码相框(十六、LCD显示JPG格式图片)
  9. 入门篇——解析Python机器学习中三类无监督学习算法和两个应用实例
  10. 如何理解Scala:迷之翻转喵 —— 协变逆变全解析