鱼骨图
用css,js画鱼骨图,效果图如下:
1、在Vue中创建 FishBoneDiagram 组件和 FishBoneLevelItem 子组件,因为子级没有规定多少级,子组件递归调用。
//FishBoneDiagram 组件
<template> <div class="FishBoneDiagram"> <div class="FishBoneContent"> <div class="FishBoneArrow"> <span class="titleRight">{{Title}}</span> </div> <template v-if="ShowPage"> <template v-for="(Level1Item,Level1ItemIdx) in mydata"> <!--第一级容器--> <div :key="Level1ItemIdx" class="Level1Content" :style="{ right:Level1Item.rightValue+'px', width:Level1Item.width+'px', height:Level1Item.height+'px', }"> <!--第一级斜线--> <div class="Level1Line" :style="{height:Level1LineHeight}"> <span @click="ItemClick(Level1Item,mydata)" class="Level1Text">{{Level1Item.label}}</span> </div> <div class="Level1ChildrenContent" :class="Level1ItemIdx%2===0?'':'bottomContent'"> <template v-if="Level1Item.hasOwnProperty('children')&&Level1Item.children.length > 0"> <template v-for="(Level2Item,Level2ItemIdx) in Level1Item.children"> <div class="Level1ChildItem" :style="{transform:`translate(${ReturnStyle(Level2Item,Level2ItemIdx,Level1Item)}px,0)`}"> <div class="LevelText Level2Text" :style="{height:`${RowH}px`,lineHeight:`${RowH}px`}"> <span @click="ItemClick(Level2Item,Level1Item)" class="text">{{Level2Item.label}}</span> <span class="lineH" :style="{width:`${Level1ChildLineW}px`}"></span> </div> <template v-if="Level2Item.hasOwnProperty('children')&&Level2Item.children.length > 0"> <FishBoneLevelItem :info="Level2Item" :index="Level2ItemIdx" :LineW="Level1ChildLineW" :level="2" :RowH="RowH" :position="Level1Item['position']" @ItemClick="ItemClick" ></FishBoneLevelItem> </template> </div> </template> </template> </div> </div> </template> </template> </div> </div> </template> <script> import FishBoneLevelItem from "./modules/FishBoneLevelItem"; export default { name: "FishBoneDiagram", components:{ FishBoneLevelItem, }, props:{ info:{}, }, data() { return { page:"鱼骨图", Title:"鱼骨图", mydata:[], ContentWidth:0, ContentHeight:0, Level1LineHeight:300,//第一级斜线长度 Level1ItemHeight:300,//第一级容器高度 Level1ItemMaxH:1,//第一级最多的子级个数 Level1ChildLineW:120,//第一级子级的线宽 RowH:20, ShowPage:false, } }, beforeDestroy() { window.removeEventListener("resize", this.ResetContentWidth); }, mounted() { window.addEventListener("resize", this.ResetContentWidth); // this.init(); }, methods: { init(info) { // console.log(info) //赋值传入的值 if(info){ this.mydata = info.data; this.Title = info.title; this.ResetContentWidth(); } }, //获取第一级容器样式。 GetLevelItem1Style(){ let mydata = JSON.parse(JSON.stringify(this.mydata)); let ItemMargin = 20; let itemWidth = ((this.ContentWidth - 100) / Math.ceil(mydata.length / 2)) - ItemMargin; for(let i = 0;i < mydata.length;i++){ let rightValue = 0; if(i <= 1){ rightValue = 50; }else{ if(i%2===0){ rightValue = this.mydata[i-1]['rightValue'] + itemWidth + ItemMargin; }else{ rightValue = this.mydata[i-2]['rightValue'] + itemWidth + ItemMargin; } } let Level1ChildLineW = itemWidth / 2; if(Level1ChildLineW > 120){ Level1ChildLineW = 120; } this.Level1ChildLineW = Level1ChildLineW; this.mydata[i]['width'] = itemWidth; this.mydata[i]['height'] = this.Level1ItemHeight; this.mydata[i]['rightValue'] = rightValue; } // console.log(JSON.parse(JSON.stringify(this.mydata))); }, //计算每一项的位置。 GetItemTextStyle(){ let mydata = this.mydata; let Level1ItemMaxH = 1;//第一级容器内最大高度 let SetItemStyle = (item)=>{ let level = item.level; let currentItem = JSON.parse(JSON.stringify(item)); if(!currentItem.hasOwnProperty('level')){ currentItem['level'] = level+1; } if(item.hasOwnProperty('children')&&item.children.length > 0){ for(let i = 0;i < item.children.length;i++){ let ArrItem = item.children[i]; ArrItem['level'] = currentItem.level+1; ArrItem['index'] = i; ArrItem['len'] = 1; SetItemStyle(ArrItem); item.len += ArrItem['len']; } } }; for(let i = 0;i < mydata.length;i++){ let itemLevel1 = mydata[i]; itemLevel1['level'] = 1; itemLevel1['index'] = i; itemLevel1['len'] = 0; if(i % 2 === 0){ itemLevel1['position'] = 'top'; }else{ itemLevel1['position'] = 'bottom'; } SetItemStyle(itemLevel1); if(Level1ItemMaxH < itemLevel1['len']){ Level1ItemMaxH = itemLevel1['len']; } } this.Level1ItemMaxH = Level1ItemMaxH; // console.log(this.Level1ItemMaxH) // console.log(JSON.parse(JSON.stringify(mydata))); }, ReturnStyle(item,itemIdx,parent){ // console.log(Level1Item) let TranslateX = 0; let RowH = this.RowH||20; let lenH = item.len; for(let i = 0;i < parent.children.length;i++){ let brotherLen = parent.children[i]; if(i > itemIdx){ lenH += brotherLen.len; } } // console.log(lenH) let HCurrent = (lenH * RowH) - (RowH / 2); if(parent['position'] === 'bottom'){ HCurrent = (parent.len - lenH) * RowH + (RowH / 2); } TranslateX = `-${HCurrent / Math.tan(Math.PI / 3)}`; TranslateX = TranslateX - 1; return TranslateX; }, ItemClick(item,parent){ // console.log(JSON.parse(JSON.stringify(item))) // console.log(JSON.parse(JSON.stringify(parent))) this.$emit("ItemClick",item,parent||{}); }, //获取容器的宽高 ResetContentWidth(){ this.ShowPage = false; this.GetItemTextStyle(); this.ContentWidth = $(".FishBoneContent")[0].clientWidth; this.ContentHeight = $(".FishBoneDiagram")[0].clientHeight; // console.log(this.ContentWidth) // console.log(this.ContentHeight) //给第一级容器高度赋值。 this.Level1ItemHeight = (this.ContentHeight - 70) / 2; //给行高赋值 let RowH = this.RowH; // console.log(this.Level1ItemHeight,this.Level1ItemMaxH) let MaxRowH = Math.ceil(this.Level1ItemHeight / this.Level1ItemMaxH); if(MaxRowH > RowH){ this.RowH = MaxRowH; } // console.log(this.RowH); //给第一级容器斜线长度赋值。 let Level1LineHeight = this.Level1ItemHeight / Math.sin(Math.PI / 3); let Level1ItemMaxH = this.Level1ItemMaxH; let MaxH = Level1ItemMaxH * this.RowH; // console.log('MaxH',MaxH); // console.log('Level1LineHeight',Level1LineHeight); if(MaxH > Level1LineHeight){ Level1LineHeight = MaxH; } this.Level1LineHeight = Level1LineHeight+'px'; //给第一级容器样式赋值。 this.GetLevelItem1Style(); this.ShowPage = true; }, } } </script> <style lang="scss"> $level2Color:#000; $level3Color: #ff8762; $level4Color: #4460ff; .FishBoneDiagram{ position: relative; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; .FishBoneContent{ position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: calc(100% - 300px); height: 10px; background-color: purple; .titleRight{ position: absolute; top: 50%; transform: translate(9px, -50%); width: 115px; min-height: 20px; line-height: 20px; font-size: 14px; word-break: break-all; } .FishBoneArrow{ position: absolute; width: 0; height: 0; right: 0; top: 50%; transform: translate(99%,-50%); border: 15px solid transparent; border-left-color: purple; } .Level1Content{ position: absolute; /*border: 1px solid #ddd;*/ width: 400px; height: 300px; top:auto; bottom:10px; right: 50px; //画斜线 .Level1Line{ position: absolute; width: 3px; transform: rotate(-30deg); transform-origin: center bottom; background-color: #93ABC7; right: 0; bottom: 0; &:before{ content:""; position: absolute; width: 0; height: 0; bottom: 0; left: 50%; transform: translate(-50%,99%); border: 5px solid transparent; border-top-color: #93ABC7; } .Level1Text{ position: absolute; top: -15px; left: 50%; transform: rotate(30deg) translate(-50%,0); font-weight: bold; font-size: 15px; cursor: pointer; } } &:nth-child(2n+1){ top: 10px; bottom: auto; .Level1Line{ top: 0; transform-origin: center top; transform: rotate(30deg); .Level1Text{ top: auto; left: 50%; bottom: -15px; transform: rotate(-30deg) translate(-50%,0); } &:before{ top: 0; left: 50%; transform: translate(-50%,-99%); border: 5px solid transparent; border-bottom-color: #93ABC7; } } } .Level1ChildrenContent{ position: absolute; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; &.bottomContent{ justify-content: flex-start; } .Level1ChildItem{ position: relative; text-align: right; .LevelText{ display: flex; align-items: center; justify-content: flex-end; height: 20px; line-height: 20px; } .text{ cursor:pointer; font-size: 12px; line-height: 14px; text-align: left; } //水平横线 .lineH{ position: relative; display: inline-block; width: 120px; border: 2px solid #000{ left: 0; right: 0; bottom: 0; } &:before{ content:">"; position: absolute; width: 10px; height: 10px; line-height: 10px; top: -5px; right: -1px; text-align: right; font-size: 17px; color: #000; } &.level3{ border-color:$level3Color; &:before{ color:$level3Color; } } &.level4{ border-color:$level4Color; &:before{ color:$level4Color; } } } } //子级容器斜线 .childrenLine{ position: absolute; top: -10px; bottom: 0; transform-origin: center top; transform: rotate(-30deg); border: 2px solid $level2Color{ top:0; right: 0; bottom: 0; }; &:before{ content:">"; position: absolute; width: 10px; height: 10px; top: -2px; left: -10px; text-align: center; transform: rotate(-90deg); font-size: 17px; color: $level2Color; } &.bottomLine{ transform: rotate(30deg); } &.level3{ border-color:$level3Color; &:before{ color:$level3Color; } } &.level4{ border-color:$level4Color; &:before{ color:$level4Color; } } } } } } } </style>
//FishBoneLevelItem 组件
<template>
<!--:style="{transform:`translate(-${120 / (level + 1)}px,0)`}"--> <div :level="level" :index="index" class="FishBoneLevelItem" :style="{transform:`translate(-${LineW / (level+1)}px,0)`}"> <span class="childrenLine" :class="(position === 'bottom'?`bottomLine level${level+1}`:`level${level+1}`)" :style="{top:`-${Math.ceil(RowH / 2)}px`,height:`${info['ContentLineH']}px`}"></span> <template v-if="ShowPage"> <template v-for="(childItem,childIdx) in info.children"> <div :key="childIdx" class="childContent" :style="{transform:`translate(${childItem['TranslateX']}px,0)`}"> <div class="LevelText childText" :style="{height:`${RowH}px`,lineHeight:`${RowH}px`}"> <span @click="ItemClick(childItem,info)" class="text">{{childItem.label}}</span> <span class="lineH" :class="`level${level+1}`" :style="{width:`${LineW / level}px`}"></span> </div> <template v-if="childItem.hasOwnProperty('children')&&childItem.children.length > 0"> <FishBoneLevelItem :info="childItem" :index="childIdx" :LineW="LineW" :level="level + 1" :RowH="RowH" :position="position" @ItemClick="ItemClick" ></FishBoneLevelItem> </template> </div> </template> </template> </div> </template> <script> //xk20211118 import FishBoneLevelItem from "./FishBoneLevelItem" export default { name: "FishBoneLevelItem", components:{ FishBoneLevelItem, }, props:{ info:{},//数据信息label,children index:{},//在父级的下标 level:{},//当前级别 LineW:{},//第二级的label右侧对应横线的宽度 RowH:{},//一行数据的高度 position:{},//位置在上面还是下面 }, data() { return { ShowPage:false, } }, mounted() { this.init(); }, methods: { init() { // console.log(JSON.parse(JSON.stringify(this.info))); this.ShowPage = false; this.GetItemStyle(); }, GetItemStyle(){ let ChildrenList = this.info.children; let ChildrenContentLen = this.info.len; let RowH = this.RowH; let ChildrenContentH = (ChildrenContentLen - 1) * RowH + ( RowH / 2); // console.log(ChildrenContentH) this.info['ContentLineH'] = ChildrenContentH / Math.sin(Math.PI / 3); for(let i = 0;i < this.info.children.length;i++){ let item = this.info.children[i]; let TranslateX = 0; let lenH = item.len; for(let j = 0;j < ChildrenList.length;j++){ let brotherLen = ChildrenList[j]; if(j > i){ lenH += brotherLen.len; } } let HCurrent = (lenH * RowH) - (RowH / 2); TranslateX = `${(ChildrenContentH - HCurrent) / Math.tan(Math.PI / 3)}`; if(this.position === 'bottom'){ TranslateX = `-${TranslateX}`; } // console.log(item.label,TranslateX) this.info.children[i]['TranslateX'] = TranslateX; } // console.log(JSON.parse(JSON.stringify(this.info))) this.ShowPage = true; }, ItemClick(item,parent){ this.$emit('ItemClick',item,parent); }, } } </script> <style lang="scss"> .FishBoneLevelItem{ position: relative; .childContent{ position: relative; } } </style>
2、在界面中引用 FishBoneDiagram 组件就可以,容器设置宽高。如下数据格式
FishBoneDiagramInfo:{
data:[
{
label:"Item1",
children:[
{
label:"阿萨德发空间",
},
{
label:"dfasdfs发放大",
children:[
{label:"方式大幅度",},
{label:"大法师",},
]
},
]
},
{
label:"Item2",
children:[
{
label:"阿萨德发空间",
children:[
{
label:'飞洒发第三方',
children:[
{label:"附近的萨克",}
]
},
{label:'发顺丰'},
]
},
{label:"就看到了发送接口范德萨"}
]
},
{
label:"Item3",
children:[
{
label:"阿萨德发空间",
children:[
{label:"fds发顺丰带头人"},
{label:"fds发顺丰带头人"},
{label:"fds发顺丰带头人"},
]
},
{label:"dfasdfs发放大"},
]
},
{
label:"Item4",
children:[
{label:"阿萨德发空间"},
{label:"dfasdfs发放大",},
]
},
],
title:"鱼骨图引用测试"
}