vue2 d3实现企查查股权穿透图股权结构图效果详解

目录
  • 前言
    • 最终效果:
    • 版本信息:
    • 股权穿透图基础功能:
    • 股权结构图基础功能:
    • 股权穿透图代码:
    • 股权结构图代码:
  • 总结:

前言

vue3 框架中使用vue2代码结合d3完成股权穿透图和股权结构图(h5)

(没错听上去很违规,但我懒得把代码从vue2改成vue3了,所以是在vue3框架里用vue2写法完成的)

最终效果:

版本信息:

"d3": "4.13.0",

"vant": "^3.1.5",

"vue": "^3.0.0",

股权穿透图基础功能:

1、默认上下游信息展示,如果没有上下游信息只展示自己

2、点击请求子节点信息展示,收起子节点

3、全屏功能

4、放大器放大缩小(react项目中不知道为啥使用d3.zoom方法不好使,可能跟网页中滚动事件冲突有关,最后选择单独防止放大器进行放大缩小功能)

5、移动功能

股权结构图基础功能:

1、tab切换展示上游或下游信息

2、默认展示一层

3、点击请求子节点信息展示,收起子节点

代码链接:github.com/QiuDaShua/v…

股权穿透图代码:

<template>
  <div class="father-box">
    <div
      id="rightPenetrationpage"
      :style="{ 'transition': 'transform .5s ease', '-ms-transition': 'transform .5s ease', '-moz-transition': 'transform .5s ease','-webkit-transition': 'transform .5s ease','-o-transition': 'transform .5s ease'}">
      <custom-nav-bar
        :title="title"
        left-arrow
        @on-clickleft="onClickLeft">
      </custom-nav-bar>
      <!-- <div
        class="full"
        @click.stop="showFullScreen">
        <div class="full-icon"></div>
        <span>{{isFull ? '退出全屏' :'全屏'}}</span>
      </div> -->
      <div
        id="penetrateChart"
        :style="{width:'100%',display:'block',margin:'auto'}"
      >
      </div>
    </div>
  </div>
</template>
<script lang="ts">
  import { defineComponent} from 'vue'
  import { useStore } from 'vuex'
  import  CustomNavBar from '@/components/common/CustomNavbar.vue'
  import { fetchCompanySearchDetail, fetchEquityUpperInfo, fetchEquityBelowInfo } from '@/api/companySearch'
  import { Notify, Toast } from 'vant'
  import { formatMoney, getBLen } from '@/utils/tool'
  import { sm2Decrypted } from '@/enrich/crypto-gm'
  import { GlobalMutation } from '@/store/types/mutation-types'
  import * as $d3 from 'd3'
  // 过渡时间
  const DURATION = 0
  // 加减符号半径
  const SYMBOLA_S_R = 9
  // 公司
  const COMPANY = '0'
  // 人
  const PERSON = '1'
  //x,y距离
  // let x0 = 0, y0 = 0, dx = 0, dy = 0
  export default defineComponent({
    props: {},
    components: {
      CustomNavBar
    },
    data () {
      return {
        layoutTree: {} as any,
        diamonds: {} as any,
        d3: $d3,
        // hasChildNodeArr: [],
        originDiamonds: {} as any,
        diagonalUp: '',
        diagonalDown: '',
        tree: {} as any,
        rootUp: {} as any,
        rootDown: {} as any,
        svg: {} as any,
        svgW: document.documentElement.clientWidth,
        svgH: document.documentElement.clientHeight - 44,
        title: '股权穿透图',
        isFull: false,
        name: '',
        id: '',
        token: '',
        regCapi: '',
        userid: '',
        parents: [] as any[], // 下游信息
        children: [] as any[], // 上游信息
      }
    },
    // beforeCreate () {
    //   document.body.style.overflow = 'hidden'
    // },
    // beforeDestroy () {
    //   document.body.style.overflow = 'auto'
    // },
    // created () {
    //   // window.addEventListener('orientationchange', this.changeOrient)
    // },
  mounted () {
      const store = useStore()
      const data = this.$route.query.data ? JSON.parse(sm2Decrypted(this.$route.query.data)) : {}
      const id = data.id
      const token = data.token ? data.token : store.state.global.token
      const userid = data.userid
      this.id = id
      this.token = token
      this.userid = userid
      store.commit(`global/${GlobalMutation.SET_TOKEN}`, token)
      Toast.loading({
        message: '加载中',
        forbidClick: true,
        duration: 0,
      });
      this.getInit()
    },
    beforeUnmount() {
      this.d3.select('#treesvg').remove()
      console.log('页面关闭')
    },
    methods: {
      // changeOrient () {
      //   const box = document.getElementById('penetrateChart').children[0]
      //   const g = document.getElementById('penetrateChart').children[0].children[0]
      //   let navbar = document.querySelector('.navbar')
      //   let flag = false
      //   flag = isOrient()
      //   setTimeout(()=>{
      //     if(flag){
      //       navbar?.classList.add('smallBar')
      //     }else{
      //       navbar?.classList.remove('smallBar')
      //     }
      //     console.log(document.documentElement.clientWidth, document.documentElement.clientHeight)
      //     box.setAttribute('width', document.documentElement.clientWidth)
      //     box.setAttribute('height', document.documentElement.clientHeight)
      //     g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')')
      //   }, 100)
      // },
      async getDetailInfo(){
        await fetchCompanySearchDetail({
          token: this.token,
          instId: this.id,
          userId: this.userid
        }).then((response)=>{
          const {code =0, records = [] } = response
          if (code > 0 && records != null) {
            this.regCapi = records[0].reg_capi
            this.name = records[0].chn_full_nm
          }
        })
      },
      async getUpper(){
        await fetchEquityUpperInfo({
          token: this.token,
          instId: this.id,
          regCapi: this.regCapi,
          currentPage: 0,
          pageSize: 200,
        }).then((response) => {
          const {code =0, records = [] } = response
          if (code > 0 && records != null) {
            const dataSource = [] as any[];
            records.forEach(element =>{
              // let children = []
              // // 设置children节点
              // if(element.list){
              //   element.list.forEach(child =>{
              //     children.push({
              //       money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--',
              //       scale: child.hold_rati || '--%',
              //       name: child.chn_full_nm || '--',
              //       id: child.inst_cust_id || '--',
              //       type: '0'
              //     })
              //   })
              // }
              dataSource.push({
                // children: children,
                isHaveChildren: element.dataType === '1' ? true : false,
                money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) : '--',
                scale: element.hold_rati || '--%',
                name: element.chn_full_nm || '--',
                id: element.inst_cust_id || '--',
                type: '0',
                regCapi: element.reg_capi
              })
            })
            this.parents = dataSource
          }
        })
      },
      async getBelow(){
        await fetchEquityBelowInfo({
          token: this.token,
          instId: this.id,
          currentPage: 0,
          pageSize: 200,
        }).then((response) => {
          const {code =0, records = []} = response
          if (code > 0 && records != null) {
            const dataSource = [] as any[];
            records.forEach(element =>{
            // let children = []
            // // 设置children节点
            // if(element.list){
            //   element.list.forEach(child =>{
            //     children.push({
            //       money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--',
            //       scale: child.hold_rati || '--%',
            //       name: child.chn_full_nm || '--',
            //       id: child.inst_cust_id || '--',
            //       type: '0'
            //     })
            //   })
            // }
            dataSource.push({
              // children: children,
              isHaveChildren: element.dataType === '1' ? true : false,
              money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
              scale: element.hold_rati || '--%',
              name: element.chn_full_nm || '--',
              id: element.inst_cust_id || '--',
              type: '0',
            })
            })
            this.children = dataSource
          }
        })
      },
      // 获取树状数据
      getTreeData(){
        console.log( this.children, this.parents, '111111111')
        let obj = {
          id: this.id,
          name: this.name,
          tap: '节点',
          children: this.children,
          parents: this.parents,
        }
        this.tree = {...obj}
        Toast.clear()
      },
      async getInit(){
        await this.getDetailInfo()
        // await this.getUpper()
        // await this.getBelow()
        Promise.all([this.getUpper(), this.getBelow()]).finally(()=>{
          this.getTreeData()
          this.init()
        })
      },
      init () {
        let d3 = this.d3
        let svgW = this.svgW
        let svgH = this.svgH
        // x0 = svgW / 2,
        // y0=  svgH / 2
        // 方块形状
        this.diamonds = {
          w: 162,
          h: 66,
          intervalW: 182,
          intervalH: 150
        }
        // 源头对象
        this.originDiamonds = {
          w: 208,
          h: 41
        }
        this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1);
        // 主图
        this.svg = d3.select('#penetrateChart').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
          .call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => {
            // 设置缩放位置以及平移初始位
            // if(isiOS && this.isFull){
            //   // 修改ios手机上才有的移动bug,安卓手机,pc端没有
            //   let x = d3.event.transform.x
            //   d3.event.transform.x = d3.event.transform.y
            //   d3.event.transform.y = -x
            //   console.log('222', '出现移动bug', d3.event.transform)
            //   console.log(isiOS, d3.event.transform.x, d3.event.transform.y)
            //   // dx = d3.event.transform.x - x0
            //   // dy = d3.event.transform.y - y0
            //   // x0 = d3.event.transform.x
            //   // y0 = d3.event.transform.y
            //   // d3.event.transform.x = d3.event.transform.x + dy
            //   // d3.event.transform.y = d3.event.transform.y + dx
            //   // this.svg.attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ') rotate(90)')
            // }
            this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
          }))
          .on('dblclick.zoom', null)
          .attr('style', 'position: relative;z-index: 2') //background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()})
          .append('g').attr('id', 'g').attr('transform', `translate(${svgW / 2},${svgH / 2})`)
        let upTree = {} as any
        let downTree = {} as any
        // 拷贝树的数据
        Object.keys(this.tree).map(item => {
          if (item === 'parents') {
            upTree = JSON.parse(JSON.stringify(this.tree))
            upTree.children = this.tree[item]
            upTree.parents = null
          } else if (item === 'children') {
            downTree = JSON.parse(JSON.stringify(this.tree))
            downTree.children = this.tree[item]
            downTree.parents = null
          }
        })
                // hierarchy 返回新的结构 x0,y0初始化起点坐标
        this.rootUp = d3.hierarchy(upTree, d => d.children);
        this.rootUp.x0 = 0
        this.rootUp.y0 = 0
        this.rootDown = d3.hierarchy(downTree, d => d.children);
        this.rootDown.x0 = 0
        this.rootDown.y0 = 0;
        // 上 和 下 结构
        let treeArr = [
          {
            data: this.rootUp,
            type: 'up'
          },
          {
            data: this.rootDown,
            type: 'down'
          }
        ]
        if(!this.tree['children'].length && !this.tree['parents'].length){
          this.updataSelf()
        }else{
          treeArr.map(item => {
          if (item.data.children) {
            // item.data.children.forEach(this.collapse);
            this.update(item.data, item.type, item.data)
          }
          })
        }
      },
      updataSelf(){
        let nodes = this.rootUp.descendants()
        let node = this.svg.selectAll('g.node')
          .data(nodes, d => d.data.id || '');
        let nodeEnter = node.enter().append('g')
          .attr('class', d => 'node node_' + d.depth) //d => showtype     === 'up' && !d.depth ? 'hide-node' :
          // .attr('transform', 'translate(' + (svgW / 2) + ',' +     (svgH / 2) + ')')
          .attr('opacity', 1); // 拥有下部分则隐藏初始块  d => showtype     === 'up' && !d.depth ? (this.rootDown.data.children.   length ? 0 : 1) : 1
        // 创建矩形
        nodeEnter.append('rect')
          .attr('type', d => d.data.id + '_' + d.depth)
          .attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20))
          .attr('height', d => d.depth ? (d.data.type === COMPANY ?  this.diamonds.h : this.diamonds.h - 10) :  this.originDiamonds.h)
          .attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 2)
          .attr('y', d => d.depth ?  0 : -15)
          .attr('stroke', '#DE4A3C')
          .attr('stroke-width', 1)
          .attr('rx', 10)
          .attr('ry', 10)
          .style('fill', d => {
            if (d.data.type === COMPANY || !d.depth) {
              return d.depth ? '#fff' : '#DE4A3C'
            } else if (d.data.type === PERSON) {
              return '#fff'
            }
          });
          // 文字
          nodeEnter.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('dy', `${this.originDiamonds.h/2 - 10}px`)
            .attr('text-anchor', 'middle')
            .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
            .text(d => d.data.name)
            .style('font-size', d => d.depth ? '16px' : '20px')
            .style('font-family', 'PingFangSC-Medium')
            .style('font-weight', '500')
      },
      /*
       *[update 函数描述], [click 函数描述]
       *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
       *  @param  {[String]} showtype up表示向上 down表示向下
       *  @param  {[Object]} sourceTree 初始源对象
       */
      update (source, showtype, sourceTree) {
        // eslint-disable-next-line
        let _this = this
        if (source.parents === null) {
          source.isOpen = !source.isOpen
        }
        let nodes
        if (showtype === 'up') {
          nodes = this.layoutTree(this.rootUp).descendants()
        } else {
          nodes = this.layoutTree(this.rootDown).descendants()
        }
        let links = nodes.slice(1);
        nodes.forEach(d => {
          d.y = d.depth *(d.depth == 1 ? 120 : this.diamonds.intervalH);
        });
        let node = this.svg.selectAll('g.node' + showtype)
          .data(nodes, d => d.data.id || '');
        let nodeEnter = node.enter().append('g')
          .attr('class', d => showtype === 'up' && !d.depth ? 'hide-node' : 'node' + showtype)
          .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + d.y + ')')
          .attr('opacity', d => showtype === 'up' && !d.depth ? (this.rootDown.data.children.length ? 0 : 1) : 1); // 拥有下部分则隐藏初始块
        // 创建矩形
        nodeEnter.append('rect')
          .attr('type', d => d.data.id)
          .attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20))
          .attr('height', d => d.depth ? (d.data.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : this.originDiamonds.h)
          .attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 2)
          .attr('y', d => d.depth ? showtype === 'up' ? -this.diamonds.h / 2 : 0 : -15)
          .attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#DE4A3C' : '#7A9EFF')
          .attr('stroke-width', 1)
          .attr('rx', 10)
          .attr('ry', 10)
          .style('fill', d => {
            if (d.data.type === COMPANY || !d.depth) {
              return d.depth ? '#fff' : '#DE4A3C'
            } else if (d.data.type === PERSON) {
              return '#fff'
            }
          });
        // 创建圆 加减
        let circle = nodeEnter.append('g')
            .attr('class', 'circle')
            .on('click', function (d) {
            _this.click(d, showtype, sourceTree)
        });
        circle.append('circle')
          .attr('type', d => d.data.id || '')
          .attr('r', (d) => d.depth ? (d.data.isHaveChildren ? SYMBOLA_S_R : 0) : 0)
          .attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : (this.diamonds.h + SYMBOLA_S_R) : 0)
          .attr('cx', 0)
          .attr('fill', '#F9DDD9')
          .attr('stroke', '#FCEDEB')
          .style('stroke-width', 1)
        circle.append('text')
          .attr('x', 0)
          .attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + this.diamonds.h / 2) : this.diamonds.h + SYMBOLA_S_R + 4) : 0)
          .attr('text-anchor', 'middle')
          .attr('class', 'fa')
          .style('fill', '#DE4A3C')
          .text(function(d) {
            if(d.depth){
              if (d.children) {
                return '-';
              } else if (d._children || d.data.isHaveChildren) {
                return '+';
              } else {
                return '';
              }
            }else {
                return '';
            }
          })
          .style('font-size', '16px');
        node.select('.fa')
        .text(function (d) {
          if (d.children) {
            return '-';
          } else if (d._children || d.data.isHaveChildren) {
            return '+';
          } else {
            return '';
          }
        })
        // 持股比例
        nodeEnter.append('g')
          .attr('transform', () => 'translate(0,0)')
          .append('text')
          .attr('x', 35)
          .attr('y', showtype === 'up' ? this.diamonds.h -10 : -10)
          .attr('text-anchor', 'middle')
          .attr('fill', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF')
          .attr('opacity', d => !d.depth ? 0 : 1)
          .text(d => d.data.scale)
          .style('font-size', '14px')
          .style('font-family', 'PingFangSC-Regular')
          .style('font-weight', '400');
        // 公司名称
        // y轴 否表源头的字体距离
        nodeEnter.append('text')
          .attr('x', 0)
          .attr('y', d => {
            // 如果是上半部分
            if (showtype === 'up') {
              // 如果是1层以上
              if (d.depth) {
                return -this.diamonds.h / 2
              } else {
                return 0
              }
            } else {
              if (d.depth) {
                return 0
              } else {
                // if (d.data.name.length > 10) {
                //   return -5
                // }
                return 0
              }
            }
          })
          .attr('dy', d => d.depth ? (d.data.name.length > 10 ? '1.3em' : '1.8em') :  `${this.originDiamonds.h/2 - 10}px`)
          .attr('text-anchor', 'middle')
          .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
          .text(d =>d.depth ? (d.data.name.length > 10) ? d.data.name.substr(0, 10) : d.data.name : d.data.name)
          .style('font-size', d => d.depth ? '14px' : '18px')
          .style('font-family', 'PingFangSC-Medium')
          .style('font-weight', '500')
          .on('click', (d) => {
            if(d.data.id  && d.depth){
              // 跳转操作之类的
            }
          });
        // 名称过长 第二段
        nodeEnter.append('text')
          .attr('x', 0)
          .attr('y', d => {
             // ? (d.depth ? -this.diamonds.h / 2 : 0) : 0
            if (showtype === 'up') {
              if (d.depth) {
                return -this.diamonds.h / 2
              }
              return 8
            } else {
              if (!d.depth) {
                return 8
              }
              return 0
            }
          })
          .attr('dy', d => d.depth ? '2.5em' : '.3em')
          .attr('text-anchor', 'middle')
          .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
          .text(d => {
            // 索引从第19个开始截取有表示超出
            if(d.depth){
              if (d.data.name.substr(19, 1)) {
                return d.data.name.substr(10, 9) + '...'
              }
              return d.data.name.substr(10, 9)
            }else{
              return null
            }
          })
          .style('font-size', '14px')
          .style('font-family', 'PingFangSC-Medium')
          .style('font-weight', '500');
        // 认缴金额
        nodeEnter.append('text')
          .attr('x', 0)
          .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
          .attr('dy', d => d.data.name.substr(10, d.data.name.length).length ? '4.5em' : '4.1em')
          .attr('text-anchor', 'middle')
          .attr('fill', d => d.depth ? '#445166' : '#fff')
          .text(d => d.data.money ? d.data.money.length > 12 ? `认缴金额:${d.data.money.substr(0, 12)}…` : `认缴金额:${d.data.money}万元` : '')
          .style('font-size', '12px')
          .style('font-family', 'PingFangSC-Regular')
          .style('font-weight', '400')
          .style('color', '#666666');
        /*
        * 绘制箭头
        * @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]
        * @param {string} viewBox 坐标系的区域
        * @param {number} markerWidth,markerHeight 标识的大小
        * @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值
        * @param {number} stroke-width 箭头宽度
        * @param {string} d 箭头的路径
        * @param {string} fill 箭头颜色
        * @param {string} id resolved0表示公司 resolved1表示个人
        * 直接用一个marker达不到两种颜色都展示的效果
        */
        nodeEnter.append('marker')
          .attr('id', showtype + 'resolved0')
          .attr('markerUnits', 'strokeWidth')
          .attr('markerUnits', 'userSpaceOnUse')
          .attr('viewBox', '0 -5 10 10')
          .attr('markerWidth', 12)
          .attr('markerHeight', 12)
          .attr('orient', '90')
          .attr('refX', () => showtype === 'up' ? '-50' : '10')
          .attr('stroke-width', 2)
          .attr('fill', '#DE4A3C')
          .append('path')
          .attr('d', 'M0,-5L10,0L0,5')
          .attr('fill', '#DE4A3C');
        nodeEnter.append('marker')
          .attr('id', showtype + 'resolved1')
          .attr('markerUnits', 'strokeWidth')
          .attr('markerUnits', 'userSpaceOnUse')
          .attr('viewBox', '0 -5 10 10')
          .attr('markerWidth', 12)
          .attr('markerHeight', 12)
          .attr('orient', '90')
          .attr('refX', () => showtype === 'up' ? '-50' : '10')
          .attr('stroke-width', 2)
          .attr('fill', '#DE4A3C')
          .append('path')
          .attr('d', 'M0,-5L10,0L0,5')
          .attr('fill', '#7A9EFF');
        // 将节点转换到它们的新位置。
        let nodeUpdate = node
          // .transition()
          // .duration(DURATION)
          .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')');
        // 将退出节点转换到父节点的新位置.
        let nodeExit = node.exit()
          // .transition()
          // .duration(DURATION)
          .attr('transform', () => showtype === 'up' ? 'translate(' + source.x + ',' + -(source.y) + ')' : 'translate(' + source.x + ',' + (parseInt(source.y)) + ')')
          .remove();
        nodeExit.select('rect')
          .attr('width', this.diamonds.w)
          .attr('height', this.diamonds.h)
          .attr('stroke', 'black')
          .attr('stroke-width', 1);
        // 修改线条
        let link = this.svg.selectAll('path.link' + showtype)
          .data(links, d => d.data.id);
        // 在父级前的位置画线。
        let linkEnter = link.enter().insert('path', 'g')
          .attr('class', 'link' + showtype)
          .attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)// 根据箭头标记的id号标记箭头
          .attr('stroke', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF')
          .style('fill-opacity', 1)
          .attr('fill', 'none')
          .attr('stroke-width', '1px')
          .attr('d', () => {
            let o = {x: source.x0, y: source.y0};
            return _this.diagonal(o, o, showtype)
          });
        let linkUpdate = linkEnter.merge(link);
        // 过渡更新位置.
        linkUpdate
          // .transition()
          // .duration(DURATION)
          .attr('d', d => _this.diagonal(d, d.parent, showtype));
        // 将退出节点转换到父节点的新位置
        link.exit()
          // .transition()
          // .duration(DURATION)
          .attr('d', () => {
            let o = {
              x: source.x,
              y: source.y
            };
            return _this.diagonal(o, o, showtype)
          }).remove();
        // 隐藏旧位置方面过渡.
        nodes.forEach(d => {
          d.x0 = d.x;
          d.y0 = d.y
        });
      },
      // 拷贝到_children 隐藏1排以后的树
      // collapse (source) {
      //   if (source.children) {
      //     source._children = source.children;
      //     source._children.forEach(this.collapse);
      //     source.children = null;
      //     this.hasChildNodeArr.push(source);
      //   }
      // },
      // 获取点击上游的上游
      async fetchUpper (id, regCapi){
        Toast.loading({
          message: '加载中',
          forbidClick: true,
          duration: 0,
        });
        const dataSource = [];
        try{
          const response = await fetchEquityUpperInfo({
            token: this.token,
            instId: id,
            currentPage: 0,
            pageSize: 200,
            regCapi: regCapi,
          })
          const {code =0, records = []} = response
          if (code > 0 && records != null) {
            const dataSource = [] as any[];
            records.forEach(element =>{
              dataSource.push({
                isHaveChildren: null,
                money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
                scale: element.hold_rati || '--%',
                name: element.chn_full_nm || '--',
                id: element.inst_cust_id || '--',
                type: '0'
              })
            })
            Toast.clear()
            return dataSource
          }else{
            Toast.clear()
            return dataSource
          }
        }catch(error){
          Toast.clear()
          return dataSource
        }
      },
      // 获取点击下游的下游
      async fetchBelow (id){
        Toast.loading({
          message: '加载中',
          forbidClick: true,
          duration: 0,
        });
        const dataSource = [];
        try{
          const response = await fetchEquityBelowInfo({
            token: this.token,
            instId: id,
            currentPage: 0,
            pageSize: 200,
          })
          const {code =0, records = []} = response
          if (code > 0 && records != null) {
            const dataSource = [] as any[];
            records.forEach(element =>{
              dataSource.push({
                isHaveChildren: null,
                money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
                scale: element.hold_rati || '--%',
                name: element.chn_full_nm || '--',
                id: element.inst_cust_id || '--',
                type: '0'
              })
            })
            Toast.clear()
            return dataSource
          }else{
            Toast.clear()
            return dataSource
          }
        }catch(error){
          Toast.clear()
          return dataSource
        }
      },
      async click  (source, showType, sourceTree) {
        // 不是起点才能点
        // if (source.depth) {
        //   if (source.children) {
        //     source._children = source.children;
        //     source.children = null;
        //   } else {
        //     source.children = source._children;
        //     source._children = null;
        //   }
        // }
        if(source.children){
          // 点击减号
          source._children = source.children;
          source.children = null;
        }else {
          // 点击加号
          if(!source._children){
            let res = [] as any[]
            if(showType === 'up'){
              res = await this.fetchUpper(source.data.id, source.data.regCapi)
            }else {
              res = await this.fetchBelow(source.data.id)
            }
            if(!res.length){
              Notify({
                message: '上游或下游企业信息为空!',
                type: 'warning',
                duration: 1500
              })
              return
            }
            res.forEach(item =>{
              let newNode = this.d3.hierarchy(item)
              newNode.depth = source.depth + 1;
              newNode.height = source.height - 1;
              newNode.parent = source;
              if(!source.children){
                source.children = [];
                source.data.children = [];
              }
              source.children.push(newNode);
              source.data.children.push(newNode.data);
            })
          }else{
            source.children = source._children;
            source._children = null;
          }
        }
        this.update(source, showType, sourceTree)
      },
      diagonal (s, d, showtype) {
          // 折线
          let endMoveNum = 0;
          let moveDistance = 0;
          if (d) {
            if (showtype == 'down') {
              let downMoveNum =  d.depth ? this.diamonds.h/2 : this.originDiamonds.h/2 -10 ;
              // var downMoveNum =  30;
              let tmpNum = s.y + (d.y - s.y) / 2;
              endMoveNum = downMoveNum;
              moveDistance = tmpNum + endMoveNum;
            } else {
              let upMoveNum = d.depth ? 0 : -this.originDiamonds.h/2 + 5 ;
              let tmpNum = d.y + (s.y - d.y) / 2;
              endMoveNum = upMoveNum;
              moveDistance = tmpNum + endMoveNum;
            }
          }
          if (showtype === 'up') {
            return (
              'M' +
              s.x +
              ',' +
              -s.y +
              'L' +
              s.x +
              ',' +
              -moveDistance +
              'L' +
              d.x +
              ',' +
              -moveDistance +
              'L' +
              d.x +
              ',' +
              -d.y
            );
          }else {
            return (
              'M' +
              s.x +
              ',' +
              s.y +
              'L' +
              s.x +
              ',' +
              moveDistance +
              'L' +
              d.x +
              ',' +
              moveDistance +
              'L' +
              d.x +
              ',' +
              d.y
            );
          }
      },
      resetSvg () {
        this.d3.select('#treesvg').remove()
        this.init()
      },
      // 点击全屏
      showFullScreen(){
        let width = document.documentElement.clientWidth,
        height = document.documentElement.clientHeight,
        wrapper = document.getElementById('rightPenetrationpage') as HTMLElement,
        style = '';
        const navbar = document.querySelector('.navbar') as HTMLElement
        const fullScreen = document.querySelector('.full') as HTMLElement
        // const box = document.getElementById('penetrateChart').children[0]
        // const g = document.getElementById('penetrateChart').children[0].children[0]
        // setTimeout(()=>{
        //   // box.setAttribute('width', width)
        //   // box.setAttribute('height', height - 44)
          // g.setAttribute('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')')
        // }, 200)
        if (this.isFull) { // 竖屏
          console.log('竖过来')
          this.isFull = false
          this.svgH = height - 44
          this.svgW = width
          // 设置按钮和顶部样式
          fullScreen.classList.remove('fullRight')
          navbar.classList.remove('smallBar')
          style += 'width:100%';
          style += 'height:100%;';
          style += '-webkit-transform: translateX(0) translateZ(0px) rotate(0); -ms-transform: translateX(0) translateZ(0px) rotate(0); -moz-transform:translateX(0) translateZ(0px)  rotate(0); -o-transform: translateX(0) translateZ(0px) rotateY(0); transform: translateX(0) translateZ(0px) rotate(0);';
          style += '-webkit-transform-origin: 0 0;';
          style += '-ms-transform-origin: 0 0;';
          style += '-moz-transform-origin: 0 0;';
          style += '-o-transform-origin: 0 0;';
          style += 'transform-origin: 0 0;';
        } else { // 横屏
          console.log('横过来')
          this.isFull = true
          this.svgH = width - 44
          this.svgW = height
          // 设置按钮和顶部样式
          fullScreen.classList.add('fullRight')
          navbar.classList.add('smallBar')
          style += 'width:' + height + 'px;';// 注意旋转后的宽高切换
          style += 'height:' + width + 'px;';
          style += '-webkit-transform: translateX(0) translateZ(0px) rotate(90deg); -ms-transform: translateX(0) translateZ(0px) rotate(90deg); -moz-transform: translateX(0) translateZ(0px) rotate(90deg); -o-transform: translateX(0) translateZ(0px) rotate(90deg); transform:translateX(0) translateZ(0px) rotate(90deg);';
          // 注意旋转中点的处理
          style += 'transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';
          style += '-webkit-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';
          style += '-ms-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';
          style += '-moz-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';
          style += '-o-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';
        }
        wrapper.style.cssText = style;
        // 重新渲染图片
        this.resetSvg()
      },
      // 点击返回
      onClickLeft(){
        // jsBridge.callHandler('navigationToSkip', {type: '0'}, ()=> {
        //   console.log('11111111111')
        // })
        history.back()
      }
    }
  })
</script>
<style lang="scss" scoped>
.father-box{
  transform: perspective(1000px);
  -ms-transform: perspective(1000px);
  -moz-transform: perspective(1000px);
  -webkit-transform: perspective(1000px);
  -o-transform: perspective(1000px);
}
.info-icon {
  width: 16px;
  height: 16px;
  background-image: url('../../assets/icon/icon_info.png');
  background-repeat: no-repeat;
  background-size: cover;
}
.full {
  position: absolute;
  top: 12px;
  right:20px;
  font-size: 14px;
  color: #DE4A3C;
  display: flex;
  z-index: 9999;
  line-height: 24px;
  .full-icon {
    width: 24px;
    height: 24px;
    background-image: url('../../assets/icon/icon_fullscreen.png');
    background-repeat: no-repeat;
    background-size: cover;
    margin-right: 7px;
  }
}
.fullRight{
  top: 12px !important;
  right:35px;
}
.smallBar {
  :deep(.van-nav-bar__left){
    display: none;
  }
  :deep(.van-nav-bar__content){
    background-color: transparent;
  }
  :deep(.van-nav-bar__title){
    // font-size: 4.8vh;
    margin-left:30px;
  }
}
</style>

股权结构图代码:

<template>
  <div
    id="structureChartIn"
    :style="{width:'100%',display:'block',margin:'auto'}"
  >
  </div>
</template>
<script lang="ts">
  // import  { setWatermark } from '@/utils/tool.js'
  import { defineComponent} from 'vue'
  import { formatMoney, getBLen } from '@/utils/tool'
  import { fetchEquityUpperInfo } from '@/api/companySearch'
  import { Notify, Toast } from 'vant'
  import * as $d3 from 'd3'
  // 过渡时间
  const DURATION = 400
  // 加减符号半径
  const SYMBOLA_S_R = 9
  // // 公司
  // const COMPANY = '0'
  // // 人
  // const PERSON = '1'
  export default defineComponent({
    props: {
      tree: {
        type: Object,
        default: () => {
          return {}
        }
      },
      token: {
        type: String,
        default: ''
      }
    },
    components: {
    },
    data () {
      return {
        diamonds: {} as any,
        originDiamonds: {} as any,
        d3: $d3,
        // hasChildNodeArr: [],
        root: {} as any,
        svg: {} as any,
        svgW: document.documentElement.clientWidth,
        svgH: document.documentElement.clientHeight - 88,
        title: '股权结构图',
        lastClickD: null,
      }
    },
    watch: {
      tree(newVal){
        if(newVal.name){
          this.init()
        }
      }
    },
    mounted () {
      // window.addEventListener('orientationchange', this.changeOrient)
    },
    // beforeUnmount(){
    //   window.removeEventListener('orientationchange', this.changeOrient)
    // },
    methods: {
      // changeOrient () {
      //   const box = document.getElementById('structureChartIn').children[0]
      //   const g = document.getElementById('structureChartIn').children[0].children[0]
      //   let navbar = document.querySelector('.navbar')
      //   let flag = false
      //   flag = isOrient()
      //   setTimeout(()=>{
      //     if(flag){
      //       navbar?.classList.add('smallBar')
      //     }else{
      //       navbar?.classList.remove('smallBar')
      //     }
      //     console.log(document.documentElement.clientWidth, document.documentElement.clientHeight)
      //     box.setAttribute('width', document.documentElement.clientWidth)
      //     box.setAttribute('height', document.documentElement.clientHeight)
      //     g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')')
      //   }, 100)
      // },
      init () {
        let d3 = this.d3
        let svgW = this.svgW
        let svgH = this.svgH
        let margin = {top: 20, right: 20, bottom: 30, left: 10}
        // 方块形状
        this.diamonds = {
          w: 320,
          h: 60,
        }
        // 源头对象
        this.originDiamonds = {
          w: 208,
          h: 36
        }
        // 主图
        this.svg = d3.select('#structureChartIn').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvgIn')
          .call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => {
            const transform = d3.event.transform
            this.svg.attr('transform', transform.translate(margin.left, margin.top));
          }))
          .on('dblclick.zoom', null)
          .attr('style', 'position: relative;z-index: 2') // background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()})
          .append('g').attr('id', 'gIn')
          .attr('transform', `translate(${margin.left},${margin.top})`)
        // 拷贝树的数据
        let downTree = {} as any
        Object.keys(this.tree).map(item => {
          if (item === 'children') {
            downTree = JSON.parse(JSON.stringify(this.tree))
            downTree.children = this.tree[item]
          }
        })
        // hierarchy 返回新的结构 x0,y0初始化起点坐标
        this.root = d3.hierarchy(downTree);
        this.root.x0 = 0
        this.root.y0 = 0
        if(!this.root.children){
          this.update(this.root)
        }else {
          // this.root.children.forEach(this.collapse)
          this.update(this.root)
        }
      },
      /*
       *[update 函数描述], [click 函数描述]
       *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
       */
      update (source) {
        // eslint-disable-next-line
        let _this = this
        let nodes= this.root.descendants()
        let index = -1, count = 0;
        this.root.eachBefore(function(n) {
          count+=20;
          n.style = 'node_' + n.depth;
          n.x = ++index * _this.diamonds.h + count;
          n.y = n.depth * 25; // 设置下一层水平位置向后移25px
        });
        let node = this.svg.selectAll('g.node')
          .data(nodes, d => {
            return d.data.id || ''
          } );
        let nodeEnter = node.enter().append('g')
          .attr('class', d => 'node node_' + d.depth)
          .attr('transform', 'translate(' + source.y0 + ',' + source.x0 + ')')
          .attr('opacity', 0);
        // 创建矩形
        nodeEnter.append('rect')
          .attr('type', d => d.data.id)
          .attr('width', d => d.depth ? this.diamonds.w : (d.data.children.length ? (getBLen(d.data.name)/2 * 18 + 62) : (getBLen(d.data.name)/2 * 18 + 20)))
          .attr('height', d => d.depth ? this.diamonds.h : this.originDiamonds.h)
          .attr('y', -this.diamonds.h / 2)
          .style('stroke', '#DE4A3C')
          .attr('stroke-width', 1)
          .attr('rx', 6)
          .attr('ry', 6)
          .style('fill', d => {
            return d.data.tap ? '#DE4A3C' : '#fff'
          });
        nodeEnter.append('rect')
          .attr('y', -this.diamonds.h / 2)
          .attr('height', d => d.depth ? this.diamonds.h : this.originDiamonds.h)
          .attr('width', 6)
          .attr('rx', 6)
          .attr('ry', 6)
          .style('fill', '#DE4A3C')
          // 文字
          nodeEnter.append('text')
            .attr('dy', d=> d.depth ? -7 : -5)
            .attr('dx', d=> d.depth ? 36 : (d.data.children.length ? 36 : 10))
            .style('font-size', d=> d.depth ? '14px' : '18px')
      .style('font-weight', '500')
      .attr('fill', d =>  d.depth ? '#333333' : '#fff')
            .text(function(d) {
        // 名字长度超过进行截取
        if(d.depth){
          if(d.data.name.length>20){
            return    d.data.name.substring(0, 19) + '...';
          }
        }
                return d.data.name;
            })
      .on('click', (d) => {
        if(d.data.id && d.depth){
            // 跳转操作之类的
        }
      });
          // 持股比例
        nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 36)
      .style('font-size', '12px')
      .style('fill', '#666666')
            .text(function(d) {
                if(!d.data.tap){
                    return ('持股比例 ' +':')
                }
            });
      nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 95)
      .style('font-size', '12px')
      .style('fill', '#DE4A3C')
            .text(function(d) {
                if(!d.data.tap){
                    return (d.data.scale)
                }
            });
      // 认缴金额
      nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 150)
      .style('font-size', '12px')
      .style('fill', '#666666')
            .text(function(d) {
                if(!d.data.tap){
                    return ('认缴金额 ' +':')
                }
            });
      nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 210)
      .style('font-size', '12px')
      .style('fill', '#DE4A3C')
            .text(function(d) {
        if(!d.data.tap){
          if(d.data.money.length > 14){
            return  d.data.money.substr(0, 14) + '...'
          }else{
            return (d.data.money + '万元')
          }
        }
            });
      // 箭头
        // nodeEnter.append('text')
        //     .attr('dy', 5.5)
        //     .attr('dx', 200 )
        //     .style('font-size', '20px')
        //     .style('fill', '#000')
        //     .text(function(d) {
        //         if(!d.data.tap){
        //             return '>'
        //         }
        //     });
      // 创造圆 加减
      let circle = nodeEnter.append('g')
            .attr('class', 'circle')
            .on('click', _this.click);
        circle.append('circle')
            .style('fill', '#F9DDD9')
            .style('stroke', '#FCEDEB')
            .style('stroke-width', 1)
            .attr('r', function (d) {
              if (d.children || d.data.isHaveChildren) {
                return 9;
              } else {
                return 0;
              }
            })
            .attr('cy', d => d.depth ? 0 : (- SYMBOLA_S_R -3))
            .attr('cx', 20)
        circle.append('text')
      .attr('dy', d => d.depth ? 4.5 : -7)
      .attr('dx', 20)
            .attr('text-anchor', 'middle')
            .attr('class', 'fa')
            .style('fill', '#DE4A3C')
            .text(function(d) {
                if (d.children) {
          return '-';
        } else if (d._children || d.data.isHaveChildren) {
          return '+';
        } else {
          return '';
        }
            })
      .style('font-size', '16px');
        node.select('.fa')
        .text(function (d) {
            if (d.children) {
        return '-';
      } else if (d._children || d.data.isHaveChildren) {
        return '+';
      } else {
        return '';
      }
        })
    /*
        * 绘制箭头
        * @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]
        * @param {string} viewBox 坐标系的区域
        * @param {number} markerWidth,markerHeight 标识的大小
        * @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值
        * @param {number} stroke-width 箭头宽度
        * @param {string} d 箭头的路径
        * @param {string} fill 箭头颜色
        */
        // nodeEnter.append('marker')
        //   .attr('id', 'resolvedIn')
        //   .attr('markerUnits', 'strokeWidth')
        //   .attr('markerUnits', 'userSpaceOnUse')
        //   .attr('viewBox', '0 -5 10 10')
        //   .attr('markerWidth', 8)
        //   .attr('markerHeight', 8)
        //   .attr('orient', '0')
        //   .attr('refX', '10')
        //   // .attr('refY', '10')
        //   .attr('stroke-width', 2)
        //   .attr('fill', '#DE4A3C')
        //   .append('path')
        //   .attr('d', 'M0,-5L10,0L0,5')
        //   .attr('fill', '#DE4A3C');
        // 将节点转换到它们的新位置。
        nodeEnter
          // .transition()
          // .duration(DURATION)
          .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; })
          .style('opacity', 1);
        node
        // .transition()
        // .duration(DURATION)
        .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; })
        .style('opacity', 1)
        .select('rect');
        // 将退出节点转换到父节点的新位置.
        let nodeExit = node.exit()
          // .transition()
          // .duration(DURATION)
          .attr('transform', () => 'translate(' + source.y + ',' + (parseInt(source.x)) + ')')
          .style('opacity', 0)
          .remove();
        // nodeExit.select('rect')
        //   .attr('width', this.diamonds.w)
        //   .attr('height', this.diamonds.h)
        //   .attr('stroke', 'black')
        //   .attr('stroke-width', 1);
        // 修改线条
        let link = this.svg.selectAll('path.link')
          .data(this.root.links(), d => d.target.id);
        // 在父级前的位置画线。
        let linkEnter = link.enter().insert('path', 'g')
          .attr('class', d =>{
            return 'link link_' + d.target.depth
          } )
          // .attr('marker-end', 'url(#resolvedIn)')// 根据箭头标记的id号标记箭头
          .attr('stroke', '#DE4A3C')
          .style('fill-opacity', 1)
          .attr('fill', 'none')
          .attr('stroke-width', '1px')
          .attr('d', () => {
            let o = {x: source.x0, y: source.y0};
            return _this.diagonal({source: o, target: o})
          })
          // .transition()
          // .duration(DURATION)
          .attr('d', _this.diagonal);
        // 过渡更新位置.
        link
          // .transition()
          // .duration(DURATION)
          .attr('d', _this.diagonal);
        // 将退出节点转换到父节点的新位置
        link.exit()
          // .transition()
          // .duration(DURATION)
          .attr('d', () => {
            let o = {
              x: source.x,
              y: source.y
            };
            return _this.diagonal({source: o, target: o})
          }).remove();
        // 隐藏旧位置方面过渡.
        this.root.each(d => {
          d.x0 = d.x;
          d.y0 = d.y
        });
      },
      // 获取点击上游的上游
      async fetchUpper (id, regCapi){
        Toast.loading({
          message: '加载中',
          forbidClick: true,
          duration: 0,
        });
        const dataSource = [];
        try{
          const response = await fetchEquityUpperInfo({
            token: this.token,
            instId: id,
            currentPage: 0,
            pageSize: 200,
            regCapi: regCapi,
          })
          const {code =0, records = []} = response
          if (code > 0 && records != null) {
            console.log(records)
            const dataSource = [] as any[];
            records.forEach(element =>{
              dataSource.push({
                isHaveChildren: null,
                money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
                scale: element.hold_rati || '--%',
                name: element.chn_full_nm || '--',
                id: element.inst_cust_id || '--',
                type: '0'
              })
            })
            Toast.clear()
            return dataSource
          }else{
            Toast.clear()
            return dataSource
          }
        }catch(error){
          Toast.clear()
          return dataSource
        }
      },
      async click  (source) {
        // if (d.children) {
        //   d._children = d.children;
        //   d.children = null;
        // } else {
        //   d.children = d._children;
        //   d._children = null;
        // }
        // if (this.lastClickD){
        //   this.lastClickD._isSelected = false;
        // }
        // d._isSelected = true;
        // this.lastClickD = d;
        if(source.children){
          // 点击减号
          source._children = source.children;
          source.children = null;
        }else {
          // 点击加号
          if(!source._children){
            let res = [] as any[]
            res = await this.fetchUpper(source.data.id, source.data.regCapi)
            if(!res.length){
              Notify({
                message: '上游或下游企业信息为空!',
                type: 'warning',
                duration: 1500
              })
              return
            }
            res.forEach(item =>{
              let newNode = this.d3.hierarchy(item)
              newNode.depth = source.depth + 1;
              newNode.height = source.height - 1;
              newNode.parent = source;
              if(!source.children){
                source.children = [];
                source.data.children = [];
              }
              source.children.push(newNode);
              source.data.children.push(newNode.data);
            })
          }else{
            source.children = source._children;
            source._children = null;
          }
        }
        this.update(source);
      },
      // 拷贝到_children 隐藏1排以后的树
      // collapse (source) {
      //   if (source.children) {
      //     source._children = source.children;
      //     source._children.forEach(this.collapse);
      //     source.children = null;
      //     this.hasChildNodeArr.push(source);
      //   }
      // },
      diagonal (d) {
        return `M ${d.source.y} ${d.source.x}
        H ${(d.source.y + (d.target.y-d.source.y)/2)}
        V ${d.target.x}
        H ${d.target.y}`;
      },
    }
  })
</script>

总结:

前端小白一枚,在之前只使用过echarts进行可视化,在开发这个功能时候发现d3版本中文网站内容较少,基本出现问题讨论也是在外文网站,踩过一堆版本的坑,最终选择稳定且例子比较多的v4版本。还有基本都是默认信息展示,很少有点击请求的功能,进行一个最终功能的整合。

以上就是vue2 d3实现企查查股权穿透图股权结构图效果详解的详细内容,更多关于vue2 d3股权穿透图结构图的资料请关注我们其它相关文章!

(0)

相关推荐

  • d3-scale d3-scaleTime使用示例详解

    目录 安装 使用 API continuous(value) continuous.invert(value) continuous.domain([domain]) continuous.range([range]) continuous.rangeRound([range]) continuous.clamp(clamp) continuous.unknown([value] continuous.interpolate(interpolate) continuous.ticks([coun

  • react hooks d3实现企查查股权穿透图结构图效果详解

    目录 前言 最终效果: 版本信息: 股权穿透图基础功能: 股权结构图基础功能: 股权穿透图代码 股权结构图代码 总结: 前言 umi+antd-admin 框架中使用hooks结合d3完成类似股权穿透图和股权结构图(web) 最终效果: 股权穿透图 股权结构图 版本信息: "d3": "4.13.0", "antd": "3.24.2", "umi": "^2.7.7", 股权穿透图基础

  • 轻松实现Android3D效果通俗易懂

    目录 一.先看看聊天(需求) 二.实现效果 三.实现 1.通过getSystemService获得SensorManager实例对象 2.通过SensorManager实例对象获得想要的传感器对象:参数决定获取哪个传感器 3.在获得焦点时注册传感器并让本类实现SensorEventListener接口 4.必须重写的两个方法 5.在失去焦点时注销传感器(为Activity提供调用) 6.draw方法中的方发详解 四.需求中的青黄色参数 五.源码 文章最后将会贴出源码(照顾新手附加注释) 一.先看

  • vue使用svg文件补充-svg放大缩小操作(使用d3.js)

    前言 项目需求是要引入svg文件,然后对里面的元素进行赋值,完了之后还要能够让svg放大缩小,点击查看全屏.针对上一篇文章,进行对svg文件里面的元素进行赋值和放大缩小的补充笔记 svg元素赋值 1. 先看看svg文件的代码 思路:我的想法就是循环拿到里面的id,然后跟后台的数据匹配,然后赋值.因为后台返回的数据id是没有后面的_C和_V 的.所以需要我前端的处理. 2. 前端js代码: onWinResize(item) { // item是后台返回的数据,我数据处理函数是单独写的,没有放在接

  • 基于d3.js/neovis.js/neod3.js实现链接neo4j图形数据库的图像化显示功能

    目录 一.使用d3.js 二. neo4jd3.js 三.neovis.js 一.基于D3.js (自由度高,写起来麻烦)二.基于neovis.js (基于d3库,简洁,但样式固定,自由度低.)三.基于neo4jd3.js (融合neovis与d3,数据格式可用d3\neo4j的,或根据需求自己重写方法) https://github.com/eisman/neo4jd3 Svg 不推荐在HTML4和XHTML中使用(但在HTML5允许) 一.使用d3.js 效果: 1.引入官方js 定义背景/

  • Bootstrap carousel轮转图的使用实例详解

    图片轮播效果在Web中常常能看到,很多人也称之为幻灯片.其主要显示的效果就是多幅图片轮回播放,从右向左播放,鼠标悬停在图片时会暂停播放,如果鼠标悬停或单击右下角圆点时,会显示对应的图片. 这种图片轮播效果,在Bootstrap框架中是通过Carousel插件来实现 演示效果截图: 代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <link rel=&q

  • C语言数据结构之图的遍历实例详解

    C语言数据结构之图的遍历实例详解 输入一组顶点,建立无向图的邻接矩阵.输入一组顶点,建立有向图的邻接表.分别对无向图和有向图进行DFS(深度优先遍历)和BFS(广度优先遍历).写出深度优先遍历的递归和非递归算法.根据建立的有向图,判断该图是否是有向无环图,若是,则输出其一种拓扑有序序列. 实现代码: #include <stdio.h> #include <stdlib.h> #define MAX 20 typedef struct ArcNode{ int adjvex; st

  • Matplotlib 折线图plot()所有用法详解

    散点图和折线图是数据分析中最常用的两种图形.其中,折线图用于分析自变量和因变量之间的趋势关系,最适合用于显示随着时间而变化的连续数据,同时还可以看出数量的差异,增长情况. Matplotlib 中绘制散点图的函数为 plot() ,使用语法如下:matplotlib.pyplot.plot(*args, scalex=True, scaley=True, data=None, **kwargs) 常用参数及说明: 参数 接收值 说明 默认值 x,y array 表示 x 轴与 y 轴对应的数据:

  • Python数据结构之图的存储结构详解

    一.图的定义 图是一种比树更复杂的一种数据结构,在图结构中,结点之间的关系是任意的,任意两个元素之间都可能相关,因此,它的应用极广.图中的数据元素通常被称为顶点 ( V e r t e x ) (Vertex) (Vertex), V V V是顶点的有穷非空集合, V R VR VR是两个顶点之间的关系的集合(可以为空),可以表示为图 G = { V , { V R } } G=\{V,\{VR\}\} G={V,{VR}}. 二.相关术语 2.1 无向图 给定图 G = { V , { E }

  • Python实现GIF动图以及视频卡通化详解

    目录 前言 环境依赖 核心代码 gif动图卡通化 视频卡通化 总结 前言 参考文章:Python实现照片卡通化 我继续魔改一下,让该模型可以支持将gif动图或者视频,也做成卡通化效果.毕竟一张图可以那就带边视频也可以,没毛病.所以继给次元壁来了一拳,我在加两脚. 项目github地址:github地址 环境依赖 除了参考文章中的依赖,还需要加一些其他依赖,requirements.txt如下: 其他环境不太清楚的,可以看我前言链接地址的文章,有具体说明. 核心代码 不废话了,先上gif代码. g

  • vue2从数据变化到视图变化之diff算法图文详解

    目录 引言 1.isUndef(oldStartVnode) 2.isUndef(oldEndVnode) 3.sameVnode(oldStartVnode, newStartVnode) 4.sameVnode(oldEndVnode, newEndVnode) 5.sameVnode(oldStartVnode, newEndVnode) 6.sameVnode(oldEndVnode, newStartVnode) 7.如果以上都不满足 小结 引言 vue数据的渲染会引入视图的重新渲染.

  • Vue使用Swiper封装轮播图组件的方法详解

    目录 Swiper 为什么要封装组件 开始封装 1.下载安装Swiper 2.引入css样式文件 3.引入js文件 4.把官网使用方法中的HTML结构复制粘贴过来 5.初始化Swiper 自定义效果 完整代码 效果展示 Swiper Swiper是一个很常用的用于实现各种滑动效果的插件,PC端和移动端都能很好的适配. 官网地址:www.swiper.com.cn/ 目前最新版本是Swiper7,但众所周知最新版本通常不稳定,所以这里使用Swiper6来封装. Swiper各版本区别: 为什么要封

  • Spark GraphX 分布式图处理框架图算法详解

    目录 正文 Graphx图结构 1. 最短路径 示例数据 可视化数据 计算最短路径 2. 网页排名 数据可视化 pagerank算法测试 算法结果 3. 连通域(连通组件) 加载图测试连通域 生成图测试 图实例的形态展示 强连接域的计算 4. 三角计数 代码测试 测试结果 5. 标签传播算法(LPA) 基本思想 正文 Spark GraphX是一个分布式图处理框架,基于 Pregel 接口实现了常用的图算法. 包括 PageRank.SVDPlusPlus.TriangleCount. Conn

  • vue利用better-scroll实现轮播图与页面滚动详解

    前言 better-scroll 也很强大,不仅可以做普通的滚动列表,还可以做轮播图.picker 等等...所以本文主要给大家介绍了关于vue用better-scroll实现轮播图与页面滚动的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1.安装better-scroll 在根目录中package.json的dependencies中添加: "better-scroll": "^0.1.15" 然后 npm i 安装. 2.封装代码

  • D3.js进阶系列之CSV表格文件的读取详解

    前言 之前在入门系列的教程中,我们常用 d3.json() 函数来读取 json 格式的文件.json 格式很强大,但对于普通用户可能不太适合,普通用户更喜欢的是用 Microsoft Excel 或 OpenOffice Calc 等生成的表格文件,因为简单易懂,容易编辑. Microsoft Excel 通常会保存为 xls 格式, OpenOffice Calc 通常会保存为 ods 格式.这些格式作为表格文件来说都很强大,但要读取它们是有些麻烦的,D3 中也没有提供这样的方法.但是表格软

随机推荐