d3学习笔记
Selection
Data
我的数据结构如下:
当我删除其中某个中间结点的时候:
我们可以清楚的看到图片在发生转动,而不是单纯的让我们选择的那个点消失,后来我终于发现了原因,就是因为没有让元素绑定数据中的独一无二的键。
const data = [
{name: "Locke", number: 4},
{name: "Reyes", number: 8},
{name: "Ford", number: 15},
{name: "Jarrah", number: 16},
{name: "Shephard", number: 23},
{name: "Kwon", number: 42}
];
d3.selectAll("div")
.data(data, function(d) { return d ? d.name : this.id; })
//或者.data(data, d => d.name) 直接将name当作创建div独一无二的键
.text(d => d.number);
Transition用法
其实原理非常简单,就是在.transition()
之前的attr赋予的属性,或者初始样式是动画的起点,之后attr赋予的属性就是动画需要变化到的终点。
d3
.selectAll("circle")
.attr("fill","red") //初始颜色为红色
.transition() //启动过渡
.duration(1000) //过渡动画持续时间 1s
.delay(500) //表示在0.5s后才开始动画
.ease("bounce") //指定过度方式
.attr("fill","steelblue") //终止颜色为铁蓝色
其中可以通过.ease()
指定过渡的方式,常用的有:
linear
:普通的线性变化circle
:慢慢地到达变换的最终状态elastic
:带有弹跳的到达最终状态bounce
:在最终状态处弹跳几次
调用时,格式形如: ease(“bounce”)。
一个完整的示例
可以尝试在join()中的exit中添加动画,让删除结点变得更加平滑
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tree map website</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="stylesheet" href="../statick/css/tree_style.css">
</head>
<!-- 页面主体 -->
<body onload="start_index()">
<div style="height: 900px;">
<!-- 头部 -->
<div id="header" class="container-fluid bg-light border rounded-2 ">
<header class="d-flex justify-content-center py-3">header</header>
</div>
<!-- 中间体 -->
<div id="middle" class="container-fluid bg-light ">
<div class="row">
<!-- 中左 -->
<div id="middle_left" class="col border rounded-2">
middle_left
</div>
<!-- 中心 -->
<div id="middle_center" class="col-auto border rounded-2">
<!-- 中间的树 -->
<svg id="center_tree">
<g id="links_g"></g>
<g id="nodes_g"></g>
</svg>
</div>
<!-- 中右 -->
<div id="middle_right" class="col border rounded-2 ">
middle_right
</div>
</div>
</div>
<!-- 底部 -->
<div id="footer" class="container-fluid bg-light border rounded-2">
<footer class="d-flex justify-content-center py-3">footer</footer>
</div>
</div>
</body>
<!-- 可以交互的脚本 -->
<script>
//全局变量
//---svg画布大小-------------------
var width = 800;
var height = 800;
//---颜色色卡数组------------------
var color_legend = ["LightPink", "DarkOrchid", "Blue", "DoderBlue", "Cyan", "DarkGreen", "PaleGodenrod", "Orange", "OrangeRed", "Gainsboro"];
//---结点与线条的数量--------------
var global_nodes_id = 0;
var global_links_id = 0;
//--------------------------------
//位置确定函数
function caculate_position(source_children_number, source_cx, source_cy) {
/*
source_children_number: 根据当前是第几个子节点来确定子节点的位置
source_cx: 父节点的x位置
source_cy: 父节点的y位置
*/
var new_cx, new_cy;
var step_length = 100
switch (source_children_number) {
case 0:
new_cx = source_cx;
new_cy = source_cy + step_length;
break;
case 1:
new_cx = source_cx - step_length;
new_cy = source_cy;
break;
case 2:
new_cx = source_cx;
new_cy = source_cy - step_length;
break;
case 3:
new_cx = source_cx + step_length;
new_cy = source_cy;
break;
default:
new_cx = source_cx;
new_cy = source_cy;
break;
}
return [new_cx, new_cy];
}
//添加的结点的字典操作---------------------------------------------
function add_tree_node_action(grapgh, source_node) {
var source_children_number = source_node.nodes_children_number;
var new_position = caculate_position(source_children_number, source_node.nodes_position.cx, source_node.nodes_position.cy)
var new_dict = {
nodes_id: global_nodes_id++,
nodes_father_id: source_node.nodes_id,
nodes_position: { cx: new_position[0], cy: new_position[1] },
nodes_children_number: 0, //新结点的子节点数为0
robustness_value: Math.random() + 0.000001,
nodes_class: Math.floor(Math.random() * 10),
nodes_confidence: Math.random()
}
return new_dict
}
//添加的链接的字典操作---------------------------------------------
function add_tree_link_action(grapgh, source_node_id, target_node_id, r_differences) {
var new_dict = {
links_id: global_links_id++,
source: source_node_id,
target: target_node_id,
robustness_differences: r_differences
}
return new_dict
}
//根据结点的编号,找到结点-----------------------------------------
function from_id_find_nodes(graph, id) {
var source_node;
var source_index = 0;
for (let i = 0; i < graph.nodes.length; i++) {
if (graph.nodes[i].nodes_id == id) {
source_node = graph.nodes[i];
source_index = i;
break;
}
}
return [source_node, source_index];
}
//添加树中的元素--------------------------------------------------
function add_tree_element(graph, source_node_id = 0) {
/*
graph: 通过修改graph数据来控制图片
source_node: 在哪个结点编号上添加结点
*/
var return_value = from_id_find_nodes(graph, source_node_id); //根据结点的id找到该结点
var source_node = return_value[0];
var source_index = return_value[1]
var new_nodes_dict = add_tree_node_action(graph, source_node);
var new_links_dict = add_tree_link_action(graph, source_node_id, new_nodes_dict
.nodes_id, Math.abs(new_nodes_dict.robustness_value - source_node.robustness_value));
graph.nodes.push(new_nodes_dict);
graph.links.push(new_links_dict);
graph.nodes[source_index].nodes_children_number++; //当前节点的孩子数量加一
graph_init_tree_node(graph)
}
//删除树中元素----------------------------------------------------
function delete_tree_element(graph, delete_node) {
//删除结点
for (let i = 0; i < graph.nodes.length; i++) {
if (graph.nodes[i].nodes_id == delete_node.nodes_id) {
console.log("当前删除结点的下标:", i)
console.log("删除前的graph", graph)
graph.nodes.splice(i, 1); //第一个参数是删除的位置,第二个参数是从这个位置删除几个元素
console.log("删除后的graph", graph)
break;
}
}
//找到连接此结点的线
for (let i = 0; i < graph.links.length; i++) {
//先修改起点
if (graph.links[i].source == delete_node.nodes_id || graph.links[i].target == delete_node.nodes_id) {
//让孩子的记录数量减一,这样再生成结点时候判断更加合理
//待实现
//建议修改孩子的数据结构用[0,1,0,0,0]这样的数组来判断哪个方向还可以生成
//---------------------------------------------------
graph.links.splice(i, 1);
i--; //这里要注意,使用splice后,整个的数组相当于在删除的位置进行了偏移
}
}
//重新画图
graph_init_tree_node(graph)
}
//计算鲁棒性与结点的半径,以及stroke
function calculate_r(robustness_value) {
var r = 1 / robustness_value * 5;
return r;
}
//通过graph绘制树-------------------------------------------------!!!
function graph_init_tree_node(graph) {
//定义该函数下的全局键盘事件
var focused = null;
d3.select("body").on("keydown", key_down_gloabal)
function key_down_gloabal(event, d) {
//在有聚焦的前提下
if (focused != null) {
//如果用户按键为Delete
if (event.key == "Delete") {
//focused.raise(); //这个一定不能少,目的就是让删除的对象图层上移,然后后续匹配数据事不会出现问题!!!!!
focused_node = focused.datum();
console.log("当前要删除的结点:", focused_node)
delete_tree_element(graph, focused_node);
}
}
}
//核心svg
center_tree_svg = d3.select("#center_tree")
.attr("width", width)
.attr("height", height);
//核心svg中的nodes_g
nodes_g = center_tree_svg.select("#nodes_g")
//核心svg中的links_g
links_g = center_tree_svg.select("#links_g")
//结点
nodes = nodes_g
.selectAll("circle")
.data(graph.nodes, d => d.nodes_id)//!!!!!!!给每个元素绑定一个键,这样d3就知道你是更新数据还是修改数据了。
.join(
enter => enter.append("circle")
.attr("cx", (d, i) => {
var source_node = from_id_find_nodes(graph, d.nodes_father_id)[0];
return source_node.nodes_position.cx;
})
.attr("cy", (d, i) => {
var source_node = from_id_find_nodes(graph, d.nodes_father_id)[0];
return source_node.nodes_position.cy;
})
.attr("r", "0px")
.transition() //过渡动画, 从之前的状态转移到现在的状态,也就是上面的状态
.duration(1000) //过渡动画持续时间 1s,
.attr("cx", (d, i) => d.nodes_position.cx)
.attr("cy", (d, i) => d.nodes_position.cy)
.attr("r", (d, i) => {
var r;
if (d.robustness_value >= 1) {
r = 5;
} else {
r = calculate_r(d.robustness_value);
}
return String(r + "px");
})
.attr("stroke", d => "rgb(211, 211, 211)")
.attr("style", d => {
var stroke_width = calculate_r(d.robustness_value) * d.nodes_confidence
return "stroke-width:" + String(stroke_width) + "px;" + "fill:" + color_legend[d.nodes_class] + ";"
}),
update => update,
exit => exit.remove())
.attr("node_id_number", d => d.nodes_id)
.classed("node", true)
.on("click", click)
.on("mouseover", node_mouseover)
.on("mouseout", node_mouseout);
//结点间的连线
links = links_g
.selectAll("line")
.data(graph.links, d => d.links_id) //!!!!!!!给每个元素绑定一个键,这样d3就知道你是更新数据还是修改数据了。
.join(
enter => enter.append("line")
.attr("x1", d => {
var source_node = from_id_find_nodes(graph, d.source)[0];
return source_node.nodes_position.cx;
})
.attr("y1", d => {
var source_node = from_id_find_nodes(graph, d.source)[0];
return source_node.nodes_position.cy;
})
.attr("x2", d => {
var source_node = from_id_find_nodes(graph, d.source)[0];
return source_node.nodes_position.cx;
})
.attr("y2", d => {
var source_node = from_id_find_nodes(graph, d.source)[0];
return source_node.nodes_position.cy;
})
.transition() //过渡动画, 从之前的状态转移到现在的状态,也就是上面的状态
.duration(1000) //过渡动画持续时间 1s
.attr("x2", d => {
var source_node = from_id_find_nodes(graph, d.target)[0];
return source_node.nodes_position.cx;
})
.attr("y2", d => {
var source_node = from_id_find_nodes(graph, d.target)[0];
return source_node.nodes_position.cy;
}),
update => update,
exit => exit.remove())
.attr("link_id_number", (d, i) => i)
.attr("style", (d, i) => {
return "stroke-width:" + String(d.robustness_differences * 10) + "px; stroke:" + color_legend[d.robustness_differences % color_legend.length] + ";"
})
// .attr("stroke-width", d => String(d.robustness_differences + 1) + "px")
// .attr("stroke", d => color_legend[d.robustness_differences % color_legend.length])
.classed("link", true) //是否将.link这个class属性赋予到这个变量
.order();
// 拖拽事件
drag = d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnded);
//缩放事件
zoom = d3.zoom()
// .extent([[0, 0], [width, height]])
// .scaleExtent([1, 8])
.on("zoom", zoomed)
//然和兴svg绑定缩放事件
center_tree_svg.call(zoom)
.on("dblclick.zoom", null) //取消自带的双击放大事件
//让node绑定拖拽事件
nodes.call(drag)
//平移缩放式事件----------------------
function zoomed(event) {
var transform = event.transform;
nodes_g.attr("transform", transform);
links_g.attr("transform", transform);
// graph_init_tree_node(graph);
}
//点击触发的事件----------------------
function click(event, d) {
var current_node_index = d.nodes_id;
add_tree_element(graph, current_node_index)
}
//结点鼠标悬停事件---------------------
function node_mouseover(event, d) {
focused = d3.select(this).attr("stroke", "rgb(43, 255, 0)");
console.log("当前的焦点focused:", focused.datum())
}
//结点鼠标移除事件---------------------
function node_mouseout(event, d) {
d3.select(this).attr("stroke", "rgb(211, 211, 211)");
focused = null; //重置聚焦为空
}
//开始拖拽时候的操作-------------------
function dragStart() {
d3.select(this).attr("stroke", "rgb(43, 255, 0)");
}
//拖拽时候的操作-----------------------
function dragging(event, d) {
d3.select(this).attr("cx", d.x = event.x).attr("cy", d.y = event.y);
var current_node_index = d.nodes_id;
d.nodes_position.cx = event.x;
d.nodes_position.cy = event.y;
change_line(current_node_index, event.x, event.y) //修改链接到此点的线
}
//拖拽结束时的操作---------------------
function dragEnded() {
// d3.select(this).attr("stroke", null);
}
//拖拽结点时候修改线条-----------------
function change_line(index, cx, cy) {
/*
index: 表示当前结点的编号
*/
for (let i = 0; i < graph.links.length; i++) {
//起点和终点都要修改
//先修改起点
links.filter(d => d.source == index)
.attr("x1", cx)
.attr("y1", cy)
//再修改终点
links.filter(d => d.target == index)
.attr("x2", cx)
.attr("y2", cy)
}
}
}
//启动页面--------------------------------------------------------
function start_index() {
graph = {
/*
图的数据,不同类型的数据是通过下标进行关联的
*/
//结点
nodes: [
{
//结点的唯一编号
nodes_id: global_nodes_id++,
//该结点的父亲结点编号
nodes_father_id: 0, //初始结点默认父亲为自己
//结点位置
nodes_position: { cx: width / 2, cy: height / 2 },
//结点的子结点个数,用来帮助确定分支的方向
nodes_children_number: 0,
//鲁棒性数值
robustness_value: 0.3,
//当前结点的图像类别
nodes_class: 0,
//当前结点的置信度
nodes_confidence: 0.5
}
],
//连线
links: [
// { links_id:0, source: 0, target: 1, robustness_differences: 0.1 },
],
};
graph = graph_init_tree_node(graph)
}
</script>
</html>