说明
【跟月影学可视化】学习笔记。
图表库 vs 数据驱动框架
- 图表库只要调用 API 就能展现内容,灵活性不高,对数据格式要求也很严格,但方便
- 数据驱动框架需要手动去完成内容的呈现,灵活,不受图表类型对应 API 的制约,但不方便
数据驱动框架不要求固定格式的数据格式,而是通过对原始数据的处理和对容器迭代、创建新的子元素,并且根据数据设置属性,来完成从数据到元素结构和属性的映射,然后再用渲染引擎将它最终渲染出来。当需求比较复杂,或者样式要求灵活多变的时候,可以考虑使用数据驱动框架。
文档
d3js 文档以及 spritejs 文档
- https://d3js.org/
- http://spritejs.com/#/
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>