说明
【跟月影学可视化】学习笔记。
图表库 vs 数据驱动框架
- 图表库只要调用 API 就能展现内容,灵活性不高,对数据格式要求也很严格,但方便
- 数据驱动框架需要手动去完成内容的呈现,灵活,不受图表类型对应 API 的制约,但不方便
数据驱动框架不要求固定格式的数据格式,而是通过对原始数据的处理和对容器迭代、创建新的子元素,并且根据数据设置属性,来完成从数据到元素结构和属性的映射,然后再用渲染引擎将它最终渲染出来。当需求比较复杂,或者样式要求灵活多变的时候,可以考虑使用数据驱动框架。
文档
d3js 文档以及 spritejs 文档
d3-selection
依赖于 DOM 操作,所以 SVG 和 SpriteJS 这种与 DOM API 保持一致的图形系统,使用起来会更加方便一些。下面将使用这个两个库进行demo的演示
使用 D3.js 绘制条形图
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>使用 D3.js 绘制条形图</title> <style> html, body { width: 100%; height: 100%; overflow: hidden; padding: 40px; margin: 0; } #stage { display: inline-block; width: 1200px; height: 600px; border: 1px dashed salmon; } </style> </head> <body> <div id="stage"></div> <script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script> <script src="https://d3js.org/d3.v6.js"></script> <script> const { Scene, SpriteSvg } = spritejs; const container = document.getElementById('stage'); // 先创建一个 Scene 对象 const scene = new Scene({ container, width: 600, height: 600, }); // 数组数据 const dataset = [125, 121, 127, 193, 309]; // 使用 D3.js 的方法对数据进行映射 // scale 函数把一组数值线性映射到某个范围,下面就是将数值映射到 500 像素区间,数值是从 100 到 309。 const scale = d3.scaleLinear() .domain([100, d3.max(dataset)]) .range([0, 500]); // 创建了一个 fglayer,它对应一个 Canvas 画布 const fglayer = scene.layer('fglayer'); // 将对应的 fglayer 元素经过 d3 包装后返回 const s = d3.select(fglayer); const colors = ['#fe645b', '#feb050', '#c2af87', '#81b848', '#55abf8']; // 在 fglayer 元素上进行迭代操作,selectAll 用来返回 fglayer 下的 sprite 子元素,表示一个图形 // 通过执行 enter() 和 append(‘sprite’),在 fglayer 下添加了 5 个 sprite 子元素。 // 再给每个 sprite 元素迭代设置属性,不同的值,就通过迭代算子来设置。 const chart = s.selectAll('sprite') .data(dataset) .enter() .append('sprite') .attr('x', 20) .attr('y', (d, i) => { return 40 + i * 95; }) .attr('width', scale) .attr('height', 80) .attr('bgcolor', (d, i) => { return colors[i]; }); // 添加坐标轴 // 通过 d3.axisBottom 创建一个底部的坐标,通过 tickValues 给坐标轴传要显示的刻度值 100, 200, 300 // 返回的 axis 函数用来绘制坐标轴,它是使用 svg 来绘制坐标轴的 const axis = d3.axisBottom(scale).tickValues([100, 200, 300]); // SpriteSvg 可以绘制一个 SVG 图形,然后将这个图形以 WebGL 或者 Canvas2D 的方式绘制到画布上。 const axisNode = new SpriteSvg({ x: 0, y: 520, }); // 通过 d3.select 选中 axisNode 对象的 svg 属性进行 svg 属性设置和创建 svg 元素操作 d3.select(axisNode.svg) .attr('width', 600) .attr('height', 520) .append('g') .attr('transform', 'translate(20, 0)') .call(axis); axisNode.svg.children[0].setAttribute('font-size', 20); // 将 axisNode 添加到 fglayer 上 fglayer.append(axisNode); </script> </body> </html>
实现效果如下:
使用 D3.js 绘制力导向图
力导向图通过模拟节点之间的斥力,来保证节点不会相互重叠。不仅能够描绘节点和关系链,而且在移动一个节点的时候,图表各个节点的位置会跟随移动,避免节点相互重叠。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>使用 D3.js 绘制力导向图</title> <style> html, body { width: 100%; height: 100%; overflow: hidden; padding: 0; margin: 0; } #stage { display: inline-block; width: 100%; height: 0; padding-bottom: 75%; } #stage canvas { background-color: seashell; } </style> </head> <body> <div id="stage"></div> <script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script> <script src="https://d3js.org/d3.v6.js"></script> <script> const { Scene } = spritejs; console.log(Scene); const container = document.getElementById('stage'); // 先创建一个 Scene 对象 const scene = new Scene({ container, width: 1200, height: 900, mode: 'stickyWidth' }); // 创建了一个 fglayer,它对应一个 Canvas 画布 const layer = scene.layer('fglayer', { handleEvent: false, autoRender: false, }); // 创建一个 d3 的力模型对象 simulation const simulation = d3.forceSimulation() .force('link', d3.forceLink().id(d => d.id)) //节点连线 .force('charge', d3.forceManyBody()) // 多实体作用 .force('center', d3.forceCenter(400, 300)); // 力中心 // 用 d3.json 来读取数据,它返回一个 Promise 对象 d3.json('./data/FeHelper-20230106175037.json').then(graph => { console.log(graph); function ticked() { d3.select(layer).selectAll('path') .attr('d', (d) => { const [sx, sy] = [d.source.x, d.source.y]; const [tx, ty] = [d.target.x, d.target.y]; return `M${sx} ${sy} L ${tx} ${ty}`; }) .attr('strokeColor', 'salmon') .attr('lineWidth', 1); d3.select(layer).selectAll('sprite') .attr('pos', (d) => { return [d.x, d.y]; }); layer.render(); } // 先用力模型来处理数据 simulation.nodes(graph.nodes).on('tick', ticked); simulation.force('link').links(graph.links); // 再绘制节点 d3.select(layer).selectAll('sprite') .data(graph.nodes) .enter() .append('sprite') .attr('pos', (d) => { return [d.x, d.y]; }) .attr('size', [10, 10]) .attr('border', [1, 'salmon']) .attr('borderRadius', 5) .attr('anchor', 0.5); // 再绘制连线 d3.select(layer).selectAll('path') .data(graph.links) .enter() .append('path') .attr('d', (d) => { const [sx, sy] = [d.source.x, d.source.y]; const [tx, ty] = [d.target.x, d.target.y]; return `M${sx} ${sy} L ${tx} ${ty}`; }) .attr('name', (d, index) => { return `path${index}`; }) .attr('strokeColor', 'salmon'); function dragsubject() { const [x, y] = layer.toLocalPos(event.x, event.y); return simulation.find(x, y); } // 将三个事件处理函数注册到 layer 的 canvas 上 d3.select(layer.canvas) .call(d3.drag() .container(layer.canvas) .subject(dragsubject) .on('start', dragstarted) .on('drag', dragged) .on('end', dragended) ); }); // dragstarted 处理开始拖拽的事件 function dragstarted(event) { // 通过前面创建的 simulation 对象启动力模拟,记录一下当前各个节点的 x、y 坐标 if (!event.active) simulation.alphaTarget(0.3).restart(); const [x, y] = [event.subject.x, event.subject.y]; event.subject.fx0 = x; event.subject.fy0 = y; event.subject.fx = x; event.subject.fy = y; // 通过 layer.toLocalPos 方法将它转换成相对于 layer 的坐标 const [x0, y0] = layer.toLocalPos(event.x, event.y); event.subject.x0 = x0; event.subject.y0 = y0; } // dragged 处理拖拽中的事件 function dragged(event) { // 转换 x、y 坐标,计算出坐标的差值,然后更新 fx、fy const [x, y] = layer.toLocalPos(event.x, event.y), { x0, y0, fx0, fy0 } = event.subject; const [dx, dy] = [x - x0, y - y0]; event.subject.fx = fx0 + dx; event.subject.fy = fy0 + dy; } // dragended 处理拖住结束事件,清空 fx 和 fy function dragended(event) { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; } </script> </body> </html>