标签云或词云是关键词的视觉化描述,是对文本中出现频率较高的关键词予以视觉上的突出,形成关键词云层或关键词渲染,从而过滤掉大量的文本信息,使浏览网页者只要一眼扫过文本就可以领略文本的主旨。
对词云不了解的同学可以加入我们「可视化团队」,「豆皮范儿」后台回复加群,欢迎咨询和交流,我们一起来做可视化库,查看词云demo进行了解。
步骤拆分绘制一个词云大致分为如下步骤
数据处理:将数据中的信息映射到单词的绘制属性,如字号、颜色、字重等。
布局算法:计算每个单词的放置位置。
绘制:将计算后的信息绘制到画布上。
实现思路这里不详细展开第一个步骤的实现,假设我们已经有了一组处理过的数据,格式如下:
const data = [ { text: '螺蛳粉', fontSize: 40, color: 'red' }, { text: '重庆小面', fontSize: 35, color: 'blue' }, { text: '肉夹馍', fontSize: 35, color: 'blue' }, { text: '炸酱面', fontSize: 32, color: 'blue' }, { text: '沙县小吃', fontSize: 25, color: 'blue' }, { text: '烤冷面', fontSize: 23, color: 'blue' }, { text: '臭豆腐', fontSize: 23, color: 'blue' }, { text: '钵钵鸡', fontSize: 20, color: 'red' }, { text: '酸辣粉', fontSize: 19, color: 'blue' }, { text: '冒菜', fontSize: 15, color: 'blue' }, { text: '驴打滚', fontSize: 12, color: 'blue' }, { text: '板栗', fontSize: 11, color: 'red' }, { text: '醪糟', fontSize: 10, color: 'blue' } ]我们需要做的就是将词汇按照权重从大到小进行排序,对于每一个单词:
选择一个初始位置
尝试放置,看是否与已经放置的单词发生重叠。如果可以放下,则记录该单词放置坐标,尝试放置下一个单词;如果不能放下,则根据布局逻辑移动到下一个位置,再次进行尝试,直到能够放下或到达放置的最外边界(即后面的位置已经不可能放下该单词了)。
如此循环直到所有的单词都尝试完毕,此时可以得到一个待放置的词汇数组,最后遍历该数组根据词汇的坐标、颜色、字体大小等信息依次绘制到画布即可。
流程图如下:
关键问题按照上述思路,实现一个简单的词云,至少需要解决两个关键问题:
文字布局算法,它决定了单词以怎样的路径尝试放置,即放置不下时获取下一个放置坐标的值。
文字碰撞算法,进行放置尝试时的重叠判断,它决定了文字是否可以放置。
文字布局算法一般情况下,词云的布局以中心为起始点,逐渐以环形向外围扩展,形成文字从中间到外围权重逐渐递减的效果。
如下图,权重大的词多数分布在靠近中心的地方,越靠外,词汇权重越低,整体呈环形向外扩展。
阿基米德螺线阿基米德螺线(亦称“等速螺线”)可以方便的实现上述布局效果,这种螺线从中心开始向外旋转,的每条臂的间距永远相等,我们可以在悬臂上取点作为放置坐标,从中心点开始放置,沿着悬臂将单词均匀的从中心向外围放置。其曲线绘制如下图:
相关公式阿基米德螺线相关方程如下:
极坐标方程:$${\displaystyle ,r=a+b\theta }$$
笛卡尔坐标系坐标公式:
$$x=(a+b∗θ)∗cosθ$$
$$y=(a+b∗θ)∗sinθ$$
其中 a 为起始点与极坐标中心的距离,b 为控制螺线间的螺距,b 越大半径 r 增长越快, 螺线越稀疏。通过不断的增加θ的值,就可以在旋臂上从里向外获取放置点。
代码实现实现archimedeanSpiral来获取坐标点,paintSpiral函数用于绘制螺线辅助观察。
/** * 阿基米德螺线, 用于初始化位置函数, 调用后返回一个获取位置的函数 * @param {*} size 画布大小, [width, height] * @param {*} { step = 0.1, b = 1, a = 0 } 步长(弧度), 螺距, 起始点距中心的距离 * @returns */ export function archimedeanSpiral(size, { step = 0.1, b = 1, a = 0 } = {}) { const e = size[0] / size[1]; // 根据画布长宽比例进行对应缩放 // 参数t为当前弧度值 return function(t) { return [e * (a + b * (t *= step)) * Math.cos(t), (a + b * t) * Math.sin(t)]; }; } /** * 辅助函数, 绘制阿基米德螺线 * @param {*} size 画布大小, [width, height] * @param {*} getPosition 布局函数, 调用archimedeanSpiral获取的返回值 * @param {*} params { showIndex } 是否显示序号 */ export function paintSpiral (size, getPosition, { showIndex = false } = {}) { const points = [] // 所有放置点 let dxdy, maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), // 最大半径 t = 1, // 阿基米德弧度 index = 0, // 当前位置序号 dx, // x坐标 dy; // y坐标 // 通过每次增加的步长固定为1,实际步长为 step * 1,来获取下一个放置点 while (dxdy = getPosition(t += 1)) { dx = dxdy[0] dy = dxdy[1] if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; // (dx, dy)距离中心超过maxDelta,跳出螺旋返回false points.push([dx, dy, index++]) } // 初始化画布 const canvas = document.createElement('canvas') canvas.width = size[0] canvas.height = size[1] canvas.style.width = size[0] canvas.style.height = size[1] const ctx = canvas.getContext('2d') ctx.fillStyle = '#f11'; ctx.strokeStyle = 'black'; let last = [0, 0] // 将放置点绘制出来 for(let point of points) { ctx.beginPath(); ctx.moveTo(last[0] + size[0] / 2, last[1] + size[1] / 2) ctx.lineTo(point[0] + size[0] / 2, point[1] + size[1] / 2) last = point ctx.stroke(); ctx.beginPath(); ctx.arc(point[0] + size[0] / 2, point[1] + size[1] / 2, 2, 0, 2 * Math.PI, false); ctx.font = '20px serif' // 绘制序号 showIndex && ctx.fillText(point[2], point[0] + size[0] / 2, point[1] + size[1] / 2) ctx.fill() } document.body.append(canvas) } 绘制图像