jQuery + d3绘制流程图
运行效果
代码
HTML代码
<!DOCTYPE html>
<html style="overflow: hidden;">
<head>
<title>流程设计工具</title>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="index.css">
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/popper.js/1.12.5/umd/popper.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.0.0-beta/js/bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.bootcss.com/d3/4.11.0/d3.min.js"></script>
<script src="https://cdn.bootcss.com/d3-transform/1.0.4/d3-transform.min.js"></script>
</head>
<body>
<div class="container-fuild">
<div id="left-wrapper" class="left-wrapper">
<ul class="sidebar-nav">
<li>
<a class="open">
<i class="fa fa-folder-o"></i>
<span>源/目标</span>
</a>
<ul>
<li class="node" data-id="101">
<a href="">
<i class="fa fa-database"></i>
<span>读数据</span>
</a>
</li>
<li class="node" data-id="102">
<a href="">
<i class="fa fa-database"></i>
<span>写数据</span>
</a>
</li>
</ul>
</li>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>数据预处理</span>
</a>
<ul>
<li class="node" data-id="211">
<a href="">
<i class="fa fa-crosshairs" aria-hidden="true"></i>
<span>类型转换</span>
</a>
</li>
<li class="node" data-id="212">
<a href="">
<i class="fa fa-crosshairs" aria-hidden="true"></i>
<span>拆分</span>
</a>
</li>
<li class="node" data-id="213">
<a href="">
<i class="fa fa-crosshairs" aria-hidden="true"></i>
<span>缺失值填充</span>
</a>
</li>
<li class="node" data-id="214">
<a href="">
<i class="fa fa-crosshairs" aria-hidden="true"></i>
<span>归一化</span>
</a>
</li>
<li class="node" data-id="215">
<a href="">
<i class="fa fa-crosshairs" aria-hidden="true"></i>
<span>标准化</span>
</a>
</li>
</ul>
</li>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>特征工程</span>
</a>
</li>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>机器学习</span>
</a>
<ul>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>二分类</span>
</a>
<ul>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>GBDT二分类</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>PS-SMART</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>线性支持向量机</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>逻辑回归二分类</span>
</a>
</li>
</ul>
</li>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>聚类</span>
</a>
<ul>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>K均值聚类</span>
</a>
</li>
</ul>
</li>
<li>
<a>
<i class="fa fa-folder-o"></i>
<span>回归</span>
</a>
<ul>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>GBDT回归</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>线性回归</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>PS_SMART回归</span>
</a>
</li>
<li class="node">
<a href="">
<i class="fa fa-circle-o"></i>
<span>PS线性回归</span>
</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<div class="middle-wrapper">
<h4>实验名称</h4>
<div id="idsw-bpmn" class="bpmn" style="position: relative; width: 100%; height: 100%;">
<svg width="100%" height="100%">
<defs>
<marker id="arrowhead" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" stroke-width="0" stroke="#333"></path>
</marker>
</defs>
</svg>
</div>
<div style="height: 40px; border-top: solid 1px #e7e7e7; text-align: center; line-height: 40px; position: absolute;bottom: 2px; width: 100%">
<a class="btn btn-link" href="#"><i class="fa fa-play-circle-o" aria-hidden="true"></i> 运行</a>
<a class="btn btn-link" href="#"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 部署</a>
<a class="btn btn-link" href="#"><i class="fa fa-share-alt" aria-hidden="true"></i> 分享</a>
</div>
</div>
<div class="right-wrapper">
<h4>实验属性</h4>
</div>
</div>
</body>
<script type="text/javascript" src="index.js"></script>
</html>
CSS代码
.left-wrapper {
width: 250px;
position: absolute;
top:0px;
bottom: 0px;
left: 0px;
background: #f0f0f0;
border-right: solid 1px #e7e7e7;
}
.middle-wrapper {
position: absolute;
top: 0px;
bottom: 0px;
left: 250px;
right: 250px;
}
.right-wrapper {
position: absolute;
width: 250px;
top: 0px;
bottom: 0px;
right: 0px;
background: #f5f5f5;
border-left: solid 1px #e7e7e7;
}
.sidebar-nav,
.sidebar-nav ul {
list-style: none;
padding: 0px;
}
.sidebar-nav > li:nth-child(odd),
.sidebar-nav ul li:nth-child(odd) {
background: #f0f0f0;
}
.sidebar-nav > li:nth-child(even),
.sidebar-nav ul > li:nth-child(even) {
background: #fff;
}
.sidebar-nav li {
text-indent: 4px;
line-height: 30px;
}
.sidebar-nav li li {
text-indent: 15px;
}
.sidebar-nav li li li {
text-indent: 25px;
}
.sidebar-nav li a {
display: block;
text-decoration: none;
color: #333;
}
.sidebar-nav li.active a {
background-color: #3e99ff;
}
.sidebar-nav li a>i+span {
margin-left: 3px;
}
.sidebar-nav li a:hover {
text-decoration: none;
background: rgba(255, 255, 255, 0.2);
}
.sidebar-nav li a:active, .sidebar-nav li a:focus {
text-decoration: none;
}
.middle-wrapper h4,
.right-wrapper h4 {
font-size: 1em;
height: 40px;
border-bottom: solid 1px #e7e7e7;
text-align: center;
line-height: 40px
}
.bpmn .node rect {
width:180px;
height:36px;
cursor: pointer;
stroke: #333;
stroke-width:2;
fill: #fff;
}
.bpmn .node.active rect,
.bpmn .node.active circle {
stroke: lightblue;
}
.bpmn .node circle {
stroke: #333;
stroke-width: 2px;
fill: #fff;
cursor: crosshair;
}
.bpmn .node circle.end {
fill: orange;
}
.bpmn .cable {
stroke: #333;
stroke-width: 2px;
fill: none;
}
JavaScript
var workflow = {
nodes: {}
};
$(function() {
var svg = d3.select("svg");
$('#left-wrapper .node').draggable({
helper: "clone",
addClass: false,
connectToSortable: "#idsw-bpmn",
start: function (e, ui) {
ui.helper.addClass("ui-draggable-helper");
},
stop: function (e, ui) {
var node = {
id: new Date().getTime(),
dataId: ui.helper.attr('data-id'),
x: ui.position.left - 250,
y: ui.position.top - 40,
text: ui.helper.text(),
inputs: 1,
outputs: 2
};
if(node.dataId == 101) {
node.inputs = 0;
node.outputs = 1;
} else if(node.dataId == 102) {
node.inputs = 1;
node.outputs = 0;
} else {
node.inputs = 1;
node.outputs = 1;
}
if(workflow.nodes[node.dataId]) {
workflow.nodes[node.dataId] += 1;
} else {
workflow.nodes[node.dataId] = 1;
}
var g = addNode(svg, node);
g.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
g.selectAll("circle.output").call(
d3.drag()
.on("start", linestarted)
.on("drag", linedragged)
.on("end", lineended)
);
g.selectAll("circle.input")
.on("mouseover", function() {
if(drawLine) {
d3.selectAll("circle.end").classed("end", false);
d3.select(this).classed("end", true);
}
});
}
});
});
var activeLine = null;
var points = [];
var translate = null;
var drawLine = false;
function linestarted() {
drawLine = false;
var anchor = d3.select(this);
var node = d3.select(this.parentNode);
var rect = node.node().getBoundingClientRect();
var dx = rect.width / (+anchor.attr("output") + 1);
var dy = rect.height;
var transform = node.attr("transform");
translate = getTranslate(transform);
points.push([dx + translate[0], dy + translate[1]]);
activeLine = d3.select("svg")
.append("path")
.attr("class", "cable")
.attr("from", node.attr("id"))
.attr("start", dx + ", " + dy)
.attr("output", d3.select(this).attr("output"))
.attr("marker-end", "url(#arrowhead)");
}
function linedragged() {
drawLine = true;
points[1] = [d3.event.x + translate[0], d3.event.y + translate[1]];
activeLine.attr("d", function() {
return "M" + points[0][0] + "," + points[0][1]
+ "C" + points[0][0] + "," + (points[0][1] + points[1][1]) / 2
+ " " + points[1][0] + "," + (points[0][1] + points[1][1]) / 2
+ " " + points[1][0] + "," + points[1][1];
});
}
function lineended(d) {
drawLine = false;
var anchor = d3.selectAll("circle.end");
if(anchor.empty()) {
activeLine.remove();
} else {
var pNode = d3.select(anchor.node().parentNode);
var input = pNode.node().getBoundingClientRect().width / (+anchor.attr("input") + 1);
anchor.classed("end", false);
activeLine.attr("to", pNode.attr("id"));
activeLine.attr("input", anchor.attr("input"));
activeLine.attr("end", input + ", 0");
}
activeLine = null;
points.length = 0;
translate = null;
}
function getTranslate(transform) {
var arr = transform.substring(transform.indexOf("(")+1, transform.indexOf(")")).split(",");
return [+arr[0], +arr[1]];
}
var dx = 0;
var dy = 0;
var dragElem = null;
function dragstarted() {
var transform = d3.select(this).attr("transform");
var translate = getTranslate(transform);
dx = d3.event.x - translate[0];
dy = d3.event.y - translate[1];
dragElem = d3.select(this);
}
function dragged() {
dragElem.attr("transform", "translate(" + (d3.event.x - dx) + ", " + (d3.event.y - dy) + ")");
updateCable(dragElem);
}
function updateCable(elem) {
var bound = elem.node().getBoundingClientRect();
var width = bound.width;
var height = bound.height;
var id = elem.attr("id");
var transform = elem.attr("transform");
var t1 = getTranslate(transform);
d3.selectAll('path[from="' + id + '"]')
.each(function() {
var start = d3.select(this).attr("start").split(",");
start[0] = +start[0] + t1[0];
start[1] = +start[1] + t1[1];
var path = d3.select(this).attr("d");
var end = path.substring(path.lastIndexOf(" ") + 1).split(",");
end[0] = +end[0];
end[1] = +end[1];
d3.select(this).attr("d", function() {
return "M" + start[0] + "," + start[1]
+ " C" + start[0] + "," + (start[1] + end[1]) / 2
+ " " + end[0] + "," + (start[1] + end[1]) / 2
+ " " + end[0] + "," + end[1];
});
});
d3.selectAll('path[to="' + id + '"]')
.each(function() {
var path = d3.select(this).attr("d");
var start = path.substring(1, path.indexOf("C")).split(",");
start[0] = +start[0];
start[1] = +start[1];
var end = d3.select(this).attr("end").split(",");
end[0] = +end[0] + t1[0];
end[1] = +end[1] + t1[1];
d3.select(this).attr("d", function() {
return "M" + start[0] + "," + start[1]
+ " C" + start[0] + "," + (start[1] + end[1]) / 2
+ " " + end[0] + "," + (start[1] + end[1]) / 2
+ " " + end[0] + "," + end[1];
});
});
}
function dragended() {
dx = dy = 0;
dragElem = null;
}
function addNode(svg, node) {
var g = svg.append("g")
.attr("class", "node")
.attr("data-id", node.dataId)
.attr("id", node.id)
.attr("transform", 'translate(' + node.x + ', ' + node.y + ')');
var rect = g.append("rect")
.attr("rx", 5)
.attr("ry", 5)
.attr("stroke-width", 2)
.attr("stroke", "#333")
.attr("fill", "#fff");
var bound = rect.node().getBoundingClientRect();
var width = bound.width;
var height = bound.height;
g.append("text")
.text(node.text)
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle");
g.append('text')
.attr("x", 18)
.attr("y", height / 2)
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr('font-family', 'FontAwesome')
.text('\uf1c0');
g.append('text')
.attr("x", width - 18)
.attr("y", height / 2)
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr('font-family', 'FontAwesome')
.text('\uf00c');
var inputs = node.inputs || 0;
g.attr("inputs", inputs);
for(var i = 0; i < inputs; i++) {
g.append("circle")
.attr("class", "input")
.attr("input", (i + 1))
.attr("cx", width * (i + 1) / (inputs + 1))
.attr("cy", 0)
.attr("r", 5);
}
var outputs = node.outputs || 0;
g.attr("outputs", outputs);
for(i = 0; i < outputs; i++) {
g.append("circle")
.attr("output", (i + 1))
.attr("class", "output")
.attr("cx", width * (i + 1) / (outputs + 1))
.attr("cy", height)
.attr("r", 5);
}
return g;
}