G6? 这么个图用什么G6? 下面的代码直接创建个Html粘贴进去,就可以看到成品。上才艺!(给个点赞关注吧!)2023-5-20:40%,雏形已建立...2023-5-21:80%了,明天再搞...2023-5-21:90%了,下午再搞...算了直接通宵:实现了90%了。下面是已经实现的效果图。2023-5-24:已完成99%了,心血来潮,突然想起,已经没有布局上的bug了。如图所示:已无bug已经实现:每个月的宽度会自动根据子集最长项之和自动伸缩文字显示和底部线段会自动根据文字长度自动伸缩我已经给你完成99%了。给你留了点,你得自己实现:子集展开收缩、线条颜色配置(我预留了,自己实现)、整体收缩展开、图自适应大小(svg是矢量图可以通过css3自由缩放或给个滚动条也行)节点事件、(可以在arr数据中加入事件,然后在创建元素的时候绑定即可)<!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>Document</title> </head> <style> #svgBox{ height: 100vh; width: 100vw; background-color: antiquewhite; } </style> <body> <div id="svgBox"></div> </body> <script> const arr = [ { label: '1月', value: 1, type: 'h' }, { label: '2月222', value: 2, type: 'h' }, { label: '3月', value: 3, type: 'h' }, { label: '4月', value: 4, type: 'h' }, { label: '5月', value: 5, type: 'h', child: [ { label: '1', value: '项目工作研讨会1', time: '2022-01-20', type: 'h1', child: [ { label: '1.1', value: '责任体系-1', time: '2022-01-20', type: 'h2', child: [ { label: '1.1.1', value: '责任体系', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '责任体系', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '责任体系', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '责任体系', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '责任体系', time: '2022-01-20', type: 'h3', } ] }, { label: '1.2', value: '动员会议-1', time: '2022-01-20', type: 'h2', child: [ { label: '1.1.1', value: '动员会议1qwer', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '动员会议2', time: '2022-01-20', type: 'h3', }, { label: '1.1.3', value: '动员会议3', time: '2022-01-20', type: 'h3', }, { label: '1.1.4', value: '动员会议4', time: '2022-01-20', type: 'h3', } ] } ] } ] }, { label: '6月', value: 6, type: 'h', child: [ { label: '2', value: '项目工作研讨会2', time: '2022-01-20', type: 'h1', child: [ { label: '1.1', value: '责任体系', time: '2022-01-20', type: 'h2', }, { label: '1.2', value: '动员会议', time: '2022-01-20', type: 'h2', } ] } ] }, { label: '7月', value: 7, type: 'h' }, { label: '8月', value: 8, type: 'h' }, { label: '9月', value: 9, type: 'h', child: [ { label: '3', value: '项目工作研讨会11', time: '2022-01-20', type: 'h1', child: [ { label: '1.1', value: '责任体系', time: '2022-01-20', type: 'h2', child: [ { label: '1.1.1', value: '动员会议1', time: '2022-01-20', type: 'h3', } ] }, { label: '1.2', value: '动员会议', time: '2022-01-20', type: 'h2', child: [ { label: '1.1.1', value: '动员会议1', time: '2022-01-20', type: 'h3', }, { label: '1.1.2', value: '动员会议2', time: '2022-01-20', type: 'h3', } ] } ] } ] }, { label: '10月', value: 10, type: 'h' }, { label: '11月', value: 11, type: 'h' }, { label: '12月', value: 12, type: 'h' } ] /** * text: string, fontWeight: string = 'normal', fontSize: string = '14px', fontFamily: string = '黑体' @return number * */ let utilCanvas = null const getTextWidth = ( text, fontWeight = 'normal', fontSize = '14px', fontFamily = '黑体' ) => { const canvas = utilCanvas || (utilCanvas = document.createElement('canvas')) const context = canvas.getContext('2d') context.font = `${fontWeight} ${fontSize} ${fontFamily}` const metrics = context.measureText(text) return Math.ceil(metrics.width) } class FishBone { constructor(arr) { this.data = arr this.oParent = document.getElementById('svgBox'); this.svgNS = "http://www.w3.org/2000/svg"; this.xMap = new Map() this.defaultBoxHeight = 30 this.defaultBoxWidth = 120 this.drawHeight = 1000 this.leafHeight = 40 this.kvArray = new Map() // 创建画布 this.oSvg = this.createTag('svg',{'xmlns':this.svgNS,'width':'100%','height': this.drawHeight + 'px'}); this.tempArr = [] // 零食存放最长子集 this.tempLen = 0 this.defultLineWidth = 220 this.childMaxHeight = 0 this.down = false this.main() } main() { this.initKvArray() this.rander() } rander() { this.randerMonthBox() this.randerFirstCircle() this.oParent.appendChild(this.oSvg); console.log(this.kvArray) } randerFirstCircle() { this.data.forEach((item, index) => { if (item.len != this.defaultBoxWidth) { this.oSvg.appendChild(this.creatBigCircle(this.xMap.get(index + 1), this.drawHeight / 2, this.down, item.child[0])); this.down = !this.down } }) } initKvArray() { this.data.forEach((item, index) => { this.tempLen = 0 item.child && item.child.length > 0 && this.getMaxWidth(item.child, 1) item.len = this.tempLen || this.defaultBoxWidth this.kvArray.set(item.value, item.len) }); } getMaxWidth(arr, level) { // 获取子集最长 arr.forEach((item, index) => { const len = getTextWidth(item.value) + this.defultLineWidth * level item.len = getTextWidth(item.value) + this.defultLineWidth if (!index) { this.tempLen = len } else { if (len > this.tempLen) { this.tempLen += len + this.defultLineWidth * level } } if (item.child && item.child.length > 0) this.getMaxWidth(item.child, level++) }) } randerMonthBox() { for (let index = 1; index <= this.data.length; index++) { let w = this.kvArray.get(index) let count = index let wcount = 0 while(count > 0) { wcount += this.kvArray.get(count) count-- } // 把每月的x坐标存起来 this.xMap.set(index, wcount - w) this.oSvg.appendChild(this.createMonth(w, this.defaultBoxHeight, 'dimgrey', this.data[index - 1].label, this.xMap.get(index), this.drawHeight / 2)); } } // 封装createTag函数 createTag (tag, objAttr) { var oTag = document.createElementNS(this.svgNS,tag); for( var attr in objAttr){ oTag.setAttribute(attr,objAttr[attr]); } return oTag; } /** 封装创建月份的方法 - 平行四边形和文字 * @param width { int } 宽 * @param height { int } 高 * @param stroke { string } 边线颜色 * @param font { string } 内部文字 * @param x { number } 绘制位置x坐标 * @param y { number } 绘制位置y坐标 */ createMonth(width=120, height=30, stroke='dimgrey', font='', x, y) { var oG = this.createTag('g', { transform: `translate(${x}, ${y})`}); var polygon = this.createTag('polygon',{'x':'0','y':'0',points: `10,0 ${width},0 ${width - 10},${height} 0, ${height}`,'fill':'#fff', stroke:`${stroke}`,'stroke-width':2}); var text = this.createTag('text', {'x':`${width / 2}`,'y':`${height / 2}`, 'font-size':'14px', 'font-weight': 'bold','fill':'#000', 'dominant-baseline': "central",'text-anchor':"middle"}); var tsapn = this.createTag('tspan'); tsapn.innerHTML=font; text.appendChild(tsapn); oG.appendChild(polygon); oG.appendChild(text); return oG } creatBigCircle(x, y, down, item) { const r = 20 // var oG = createTag('g', { transform: `translate(${x}, ${y})`}); const halfR = r * 1.5 let yc = 0 let yl1 = 0 let yl2 = 0 let lx = r + 5 // 是否向下拓展 if (down) { yc = y + this.defaultBoxHeight + halfR yl1 = -r yl2 = -halfR } else { yc = y - halfR yl1 = r yl2 = halfR } var g = this.createTag('g', { transform: `translate(${x + 2 * r}, ${yc})`}) var bigCircle = this.createTag('circle', { cx: 0, cy: 0, r: r, stroke: 'dimgrey', 'stroke-width': 2, fill:"white" }) var line = this.createTag('line', { x1: 0, y1: yl1, x2: 0, y2:yl2, stroke: 'dimgrey', 'stroke-width': 2 }) var text = this.createTag('text', {'x':0,'y':0, 'font-size':'16px', 'font-weight': 'bold','fill':'#000', 'dominant-baseline': "middle",'text-anchor':"middle"}); var tsapn = this.createTag('tspan'); tsapn.innerHTML=item.label; text.appendChild(tsapn); var titleline = this.createTag('line', { x1: lx, y1: 0, x2: lx + this.getLineWidth(item), y2:0, stroke: 'dimgrey', 'stroke-width': 2 }) var textTime = this.createTag('text', {'x':30,'y': 0, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTime = this.createTag('tspan'); tsapnTime.innerHTML=item.time; textTime.appendChild(tsapnTime); var textTitle = this.createTag('text', {'x':110,'y': 0, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTitle = this.createTag('tspan'); tsapnTitle.innerHTML=item.value; textTitle.appendChild(tsapnTitle); g.appendChild(bigCircle); g.appendChild(line); g.appendChild(text); g.appendChild(titleline); g.appendChild(textTime); g.appendChild(textTitle); let prev = 0 // 记录每组中上一个的高度,因为下一个高度=上一个的高度+当前高度 item.child.forEach((ele) => { prev = this.renderParent(g,ele, down, r, prev) }) return g } getLineWidth(item, leaf) { var lineWidth = (getTextWidth(item.time) + 20 + getTextWidth(item.value)) if (leaf == 'leaf') { lineWidth = (getTextWidth(item.time, 'normal','12px') + 40 + getTextWidth(item.value,'normal','12px')) } var curLineWidth = lineWidth < 220 ? 220 : lineWidth return lineWidth } renderParent(pg,item, down, r, prev) { // console.log(item, 'down'); this.childMaxHeight = 0 var haflTh = 0 if (item.child) { // 获取所有子集最长项, 既数组最长的子集也是最长的数组. 然后遍历每一个子集, 计算其高度并根据当前高 this.getMaxChildHeight(item.child) haflTh = this.childMaxHeight * this.leafHeight } else { // 没有子集就走这个 haflTh = this.childMaxHeight * this.leafHeight / 2 + 50 + prev } // 把最长项*子集高度的一半+初始高度+上一个的高度 var h = this.childMaxHeight * this.leafHeight / 2 + 50 + prev var g = this.createTag('g') var points = `0,-20 0,-${h} 10,-${h}` var cx = 10+r var cy = -h if (down) { points = `0,20 0,${h} 10,${h}` cy = h } var xStart = cx + r + 5 var xEnd = xStart + this.getLineWidth(item) var polyline = this.createTag('polyline',{points: points,'fill':'none', stroke:`dimgrey`,'stroke-width':2}); var bigCircle = this.createTag('circle', { cx: cx, cy: cy, r: r, stroke: 'dimgrey', 'stroke-width': 2, fill:"white" }) var line = this.createTag('line', { x1: xStart, y1: cy, x2: xEnd, y2: cy, stroke: 'dimgrey', 'stroke-width': 2 }) var text = this.createTag('text', {'x':cx,'y':cy, 'font-size':'14px', 'font-weight': 'bold','fill':'#000', 'dominant-baseline': "middle",'text-anchor':"middle"}); var tsapn = this.createTag('tspan'); tsapn.innerHTML=item.label; var textTime = this.createTag('text', {'x':xStart,'y': cy, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTime = this.createTag('tspan'); tsapnTime.innerHTML=item.time; textTime.appendChild(tsapnTime); var textTitle = this.createTag('text', {'x':xStart + 60 + 20,'y': cy, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTitle = this.createTag('tspan'); tsapnTitle.innerHTML=item.value; textTitle.appendChild(tsapnTitle); text.appendChild(tsapn); g.appendChild(polyline); g.appendChild(bigCircle); g.appendChild(text); g.appendChild(line); g.appendChild(textTime); g.appendChild(textTitle); pg.appendChild(g) if (item.child && item.child.length > 0) { var startY = this.leafHeight * (item.child.length - 1) / 2 console.log(startY, 'startY'); item.child && item.child.forEach((ele, index) => { this.randerLeaf(g, xEnd, cy, ele, startY) startY -= this.leafHeight }) } return haflTh } randerLeaf(pg, sx, sy, item, startY) { var g = this.createTag('g', { transform: `translate(${sx}, ${sy})`}) var xEnd = 70 + 5 + this.getLineWidth(item, 'leaf') var points = `0,0 0,${startY} 20,${startY}` var polyline = this.createTag('polyline', { points: points,'fill':'none', stroke:`dimgrey`,'stroke-width':2 }); var polygon = this.createTag('polygon',{points: `25,${startY - 10} 80,${startY - 10} 70,${startY + 10} 15, ${startY + 10}`,'fill':'#fff', stroke:'dimgrey','stroke-width':2}); var line = this.createTag('line', { x1: 80, y1: startY, x2: xEnd, y2: startY, stroke: 'dimgrey', 'stroke-width': 2 }) var text = this.createTag('text', {'x': 45,'y': startY, 'font-size':'12px', 'font-weight': 'bold','fill':'#000', 'dominant-baseline': "central",'text-anchor':"middle"}); var tsapn = this.createTag('tspan'); tsapn.innerHTML=item.label; text.appendChild(tsapn); var textTime = this.createTag('text', {'x':85,'y': startY, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTime = this.createTag('tspan'); tsapnTime.innerHTML=item.time; textTime.appendChild(tsapnTime); var textTitle = this.createTag('text', {'x':85 + 60 + 20,'y': startY, 'font-size':'12px', 'font-weight': 'normal','fill':'#000', 'dominant-baseline': "ideographic",'text-anchor':"left"}); var tsapnTitle = this.createTag('tspan'); tsapnTitle.innerHTML=item.value; textTitle.appendChild(tsapnTitle); g.appendChild(polyline); g.appendChild(polygon); g.appendChild(line); g.appendChild(text); g.appendChild(textTime); g.appendChild(textTitle); pg.appendChild(g) } getMaxChildHeight(arr) { if (arr.length > this.childMaxHeight) { this.childMaxHeight = arr.length } arr.forEach(item => { if (item.child && item.child.length > 0) { this.getMaxChildHeight(item.child) } }) } } new FishBone(arr) </script> </html>
G6? 这么个图用什么G6? 下面的代码直接创建个Html粘贴进去,就可以看到成品。上才艺!(给个点赞关注吧!)

2023-5-20:40%,雏形已建立...
2023-5-21:80%了,明天再搞...
2023-5-21:90%了,下午再搞...
算了直接通宵:实现了90%了。下面是已经实现的效果图。
2023-5-24:已完成99%了,心血来潮,突然想起,已经没有布局上的bug了。如图所示:
已无bug
已经实现:
每个月的宽度会自动根据子集最长项之和自动伸缩
文字显示和底部线段会自动根据文字长度自动伸缩
我已经给你完成99%了。
给你留了点,你得自己实现:
子集展开收缩、
线条颜色配置(我预留了,自己实现)、
整体收缩展开、
图自适应大小(svg是矢量图可以通过css3自由缩放或给个滚动条也行)
节点事件、(可以在arr数据中加入事件,然后在创建元素的时候绑定即可)