D3.js 实现带伸缩时间轴拓扑图的示例代码

作者:Assan 时间:2024-05-09 10:20:13 

效果图:

D3.js 实现带伸缩时间轴拓扑图的示例代码

基于d3-v5, 依赖dagre-d3, 直接上代码:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
 svg {
  border: 1px solid darkcyan;
 }

/* 拓扑图--start */
 /* 节点状态颜色 */
 g.type-current>circle {
  fill: #FFAC27;
 }

g.type-success>circle {
  fill: #9270CA;
 }

g.type-fail>circle {
  fill: #67C23A;
 }

g.type-done>circle {
  fill: #E8684A;
 }

/* 拓扑图--end */

/* 坐标轴-start */
 .axis path,
 .axis line {
  fill: none;
  stroke: #DCDCDC;
  shape-rendering: crispEdges;
 }

.axis text {
  font-family: sans-serif;
  font-size: 12px;
  fill: #999999;
 }

.axis .x2-axis text {
  font-size: 14px;
  font-weight: 400;
  fill: #333;
 }

.axis .x2-axis .tick {
  stroke-width: 2px;
 }

/* 坐标轴-end */
</style>
</head>
<script src=" http://d3js.org/d3.v5.min.js "></script>
<script src="https://cdn.bootcss.com/dagre-d3/0.6.3/dagre-d3.js"></script>

<body>
</body>
<script>

let nodeInfo = [{
 id: 0,
 label: "",
 status: 'success',
 date: 1575129600000
}, {
 id: 1,
 label: "",
 status: 'fail',
 date: 1578376890000
}, {
 id: 2,
 label: '',
 status: 'success',
 date: 1578376890000
}, {
 id: 3,
 label: '',
 status: 'fail',
 date: 1578895290000
}, {
 id: 4,
 label: '',
 status: 'current',
 date: 1578895290000
}, {
 id: 5,
 label: '',
 status: 'done',
 date: 1579327290000
}, {
 id: 6,
 label: '',
 status: 'done',
 date: 1579932090000
}, {
 id: 7,
 label: '',
 status: 'done',
 date: 1581487290000
}, {
 id: 8,
 label: '',
 status: 'success',
 date: 1583461994000
}]
let lineInfo = [
 { from: 0, to: 1 },
 { from: 0, to: 2 },
 { from: 0, to: 3 },
 { from: 2, to: 4 },
 { from: 2, to: 5 },
 { from: 3, to: 6 },
 { from: 6, to: 7 },
 { from: 6, to: 8 },
]

let nodeMap = new Map() //节点信息map
let nodeDomMap = new Map() //节点dom--map
let timeArr = [] //存储时间

const width = 1200
const height = 400
const padding = { top: 0, bottom: 40, left: 40, right: 40 }

// 节点信息转化为map
nodeInfo.forEach(item => {
 nodeMap.set(item.id, item);
 timeArr.push(item.date)
})
let max = new Date(d3.max(timeArr))
let min = new Date(d3.min(timeArr))
maxY = max.getFullYear()
maxM = max.getMonth()
minY = min.getFullYear()
minM = min.getMonth()

// 创建画布 svg
let svg = d3.select("body").append("svg")
 .attr("id", "svg-canvas")
 .attr("preserveAspectRatio", "xMidYMid meet")
 .attr("viewBox", `0 0 ${width} ${height}`)

// 初始化元素
let background = svg.append("rect").attr("class", "bg")
let view = svg.append("g").attr("class", "view")
let grid = svg.append("g").attr("class", "grid")
let axis = svg.append("g").attr("class", "axis")
let separateLine = svg.append("line").attr("class", "separate-line")

// 绘制箭头以供引用
d3.select("#svg-canvas").append("defs").append("marker")
 .attr("id", "triangle").attr("viewBox", "0 0 10 10")
 .attr("refX", "17").attr("refY", "5")
 .attr("markerWidth", "6").attr("markerHeight", "6")
 .attr("orient", "auto").append("path")
 .attr("d", "M 0 0 L 10 5 L 0 10 z").style("fill", "#bbbbbb")

// 添加背景板 rect
background.attr("fill", "#FAFAFA")
 .attr("x", 0).attr("y", 0)
 .attr("width", width).attr("height", height - padding.bottom)
const monthNum = d3.timeMonth.count(min, max) // 区间月份数量

// 确定比例尺
let xScale = d3.scaleTime()
 .domain([new Date(minY, minM, 1), new Date(maxY, ++maxM, 1)])
 .range([0, width - padding.left - padding.right])

// 坐标轴文本格式化
let formatDay = d3.axisBottom(xScale).tickFormat((d, i) => {
 const date = new Date(d)
 const day = date.getDate()
 return `${day === 1 ? "" : day}` // 如果是1号, 不显示刻度,直接由xAxis2显示年月
})
let formatMonth = d3.axisBottom(xScale).ticks(d3.timeMonth.every(1)).tickPadding(6).tickSizeInner(20).tickFormat((d, i) => {
 const date = new Date(d)
 const mon = date.getMonth() + 1
 const year = date.getFullYear()
 return `${year} - ${mon > 9 ? mon : "0" + mon}`
})
axis.attr('transform', `translate(${padding.left},${height - padding.bottom})`)
let xAxisDay = axis.append("g")
 .attr("class", "x-axis").call(formatDay)
let xAxisMonth = axis.append("g")
 .attr("class", "x2-axis").call(formatMonth)

// 绘制x网格
const lineGroup = grid.attr("transform", `translate(${padding.left},0)`)
 .selectAll("g")
 .data(xScale.ticks(monthNum))
 .enter().append("g")
lineGroup.append("line")
 .attr("x1", d => { return xScale(new Date(d)) })
 .attr("x2", d => { return xScale(new Date(d)) })
 .attr("y1", padding.top)
 .attr("y2", height - padding.bottom)
 .attr("class", "grid-line")
 .style("stroke", "#DCDCDC")
 .style("stroke-dasharray", 6)

// 添加坐标轴与拓扑图分隔线
separateLine.style("stroke", "#DCDCDC")
 .style("stroke-width", 2)
 .attr("x1", 0)
 .attr("x2", width)
 .attr("y1", height - padding.bottom)
 .attr("y2", height - padding.bottom)

// 绘制流程图 节点--箭头
let g = new dagreD3.graphlib.Graph()
 .setGraph({})
 .setDefaultEdgeLabel(function () { return {}; });
g.graph().rankdir = "LR"; // 控制水平显示
g.graph().marginx = 0;
g.graph().marginy = 50;

nodeInfo && nodeInfo.map((item, i) => {
 g.setNode(item.id, {
  label: item.label,
  class: "type-" + item.status,
  style: "stroke-width: 2px; stroke: #fff",
  shape: "circle",
  id: item.id
 });

})

lineInfo && lineInfo.map((item, i) => {
 g.setEdge(item.from, item.to,
  {
   arrowheadStyle: "stroke:none; fill: none", // 箭头头部样式
   style: "stroke:none; fill: none" //线条样式
  })

})

let render = new dagreD3.render();
render(view.attr("transform", `translate(${padding.left},0)`), g);

// 重新定位节点x坐标
const nodesArr = d3.select(".nodes").selectAll(".node")._groups[0]
nodesArr.forEach((item) => {
 let dom = d3.select(item)._groups[0][0]
 let id = Number(dom.id)
 let date = nodeMap.get(id).date
 const x = xScale(new Date(date));
 const y = dom.transform.animVal[0].matrix.f
 d3.select(item).attr("transform", `translate(${x},${y})`)
 nodeDomMap.set(Number(item.id), item)
})

// 重新绘制箭头
lineInfo && lineInfo.map((item, i) => {
 let fromDom = nodeDomMap.get(Number(item.from))
 let toDom = nodeDomMap.get(Number(item.to))
 const [x1, y1, x2, y2] = [
  fromDom.transform.animVal[0].matrix.e,
  fromDom.transform.animVal[0].matrix.f,
  toDom.transform.animVal[0].matrix.e,
  toDom.transform.animVal[0].matrix.f,
 ]
 d3.select(".edgePaths").append("g")
  .append("line")
  .attr("class", `to-${item.to}`) // 设置唯一的class方便修改路径
  .attr("stroke-width", "2")
  .attr("stroke", "#bbbbbb")
  .style("stroke-dasharray", 8)
  .attr("marker-end", "url(#triangle)")
  .attr("x1", x1).attr("y1", y1)
  .attr("x2", x2).attr("y2", y2)

})

// 设置zoom参数
let zoom = d3.zoom()
 .scaleExtent([1, 10])
 .translateExtent([[0, 0], [width, height]]) //移动的范围
 .extent([[0, 0], [width, height]])//视窗 (左上方,右下方)

svg.call(zoom.on("zoom", reRender.bind(this)));

// 每次缩放重定位渲染拓扑图
function reRender() {
 const t = d3.event.transform.rescaleX(xScale) //获得缩放后的比例尺
 xAxisDay.call(formatDay.scale(t))  //重新设置x坐标轴的scale
 xAxisMonth.call(formatMonth.scale(t))  //重新设置x坐标轴的scale

const view = d3.select(".output")
 const axis = d3.select(".axis-month")
 const grid = d3.selectAll(".grid-line")

// 重新绘制节点
 nodesArr.forEach((item) => {
  let dom = d3.select(item)._groups[0][0]
  let id = Number(dom.id)
  let date = nodeMap.get(id).date
  const x = t(new Date(date));
  const y = dom.transform.animVal[0].matrix.f
  d3.select(item).attr("transform", `translate(${x},${y})`)
  nodeDomMap.set(Number(item.id), item)
 })

// 重新绘制箭头
 lineInfo && lineInfo.map((item, i) => {
  let fromDom = nodeDomMap.get(Number(item.from))
  let toDom = nodeDomMap.get(Number(item.to))
  const [x1, y1, x2, y2] = [
   fromDom.transform.animVal[0].matrix.e,
   fromDom.transform.animVal[0].matrix.f,
   toDom.transform.animVal[0].matrix.e,
   toDom.transform.animVal[0].matrix.f,
  ]
  d3.select(`.to-${item.to}`)
   .attr("x1", x1).attr("y1", y1)
   .attr("x2", x2).attr("y2", y2)

})

//重新绘制x网格
 svg.selectAll(".grid-line")
  .attr("x1", d => { return t(new Date(d)) })
  .attr("x2", d => { return t(new Date(d)) })
}

</script>

</html>

来源:https://segmentfault.com/a/1190000021623664

标签:D3.js,时间轴拓扑图
0
投稿

猜你喜欢

  • 利用XSLT把ADO记录集转换成XML

    2008-09-05 17:12:00
  • CSS清除浮动常用方法小结

    2009-07-07 11:59:00
  • asp一个空间绑定N个域名的方法!

    2009-03-08 18:32:00
  • Python入门教程(一)Python简单介绍

    2023-10-25 03:19:16
  •  python中字符串的常见操作总结(二)

    2023-04-08 17:17:35
  • python 下载文件的几种方式分享

    2021-03-27 14:08:17
  • linux系统oracle数据库出现ora12505问题的解决方法

    2024-01-27 09:06:15
  • javascript获取ckeditor编辑器的值(实现代码)

    2024-04-16 09:12:54
  • django创建简单的页面响应实例教程

    2021-05-15 17:05:08
  • JS实现的网页背景闪电闪烁效果代码

    2024-04-16 09:01:19
  • Python使用matplotlib实现交换式图形显示功能示例

    2023-04-25 06:10:49
  • Django ModelForm操作及验证方式

    2021-04-22 18:11:44
  • Python实现的合并两个有序数组算法示例

    2021-07-11 10:44:10
  • Python通过DOM和SAX方式解析XML的应用实例分享

    2023-10-15 10:46:32
  • Python学习之流程控制与条件判断总结

    2023-08-20 13:37:02
  • SQL--JOIN之完全用法

    2008-09-12 17:30:00
  • Mysql中二进制日志操作方法说明

    2024-01-25 23:08:28
  • xml xpath基础语法

    2008-01-21 12:46:00
  • 详解MySQL的用户密码过期功能

    2024-01-21 01:29:40
  • python的命名规则知识点总结

    2023-08-22 09:48:21
  • asp之家 网络编程 m.aspxhome.com