鱼骨图


用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:"鱼骨图引用测试"
        }

posted @ 2021-11-18 17:07  1忘记  阅读(1787)  评论(0编辑  收藏  举报