股权穿透d3

<template>
    <div class="penetrate-chart">
        <div class="bt-group">
            <button class="save" @click="saveImg">保存</button>
            <button class="reset" @click="resetSvg">重置</button>
        </div>
    </div>
</template>
<!--股权穿透图-->
<script>
import * as d3 from 'd3'
// 过渡时间
const DURATION = 0
// 加减符号半径
const SYMBOLA_S_R = 9
// 公司
const COMPANY = 0
// 人
const PERSON = 1
export default {
    props: {},

    components: {},

    data() {
        return {
            layoutTree: '',
            diamonds: '',
            d3: d3,
            i: 0,
            hasChildNodeArr: [],
            originDiamonds: '',
            diagonalUp: '',
            diagonalDown: '',
            tree: { "name": "多多包", "children": [{ "name": "一卡通公司", "type": 0 }, { "name": "一卡通公司2", "type": 0, "children": [{ "name": "小公司", "type": 0, "children": [{ "name": "小小小", "type": 0, "children": [{ "type": 1, "name": "笑小下" }] }] }, { "type": 0, "name": "小公司2" }] }, { "name": "一卡通公司2333", "type": 0, "children": [{ "type": 0, "name": "小公司" }, { "type": 0, "name": "小公司2" }] }, { "type": 0, "name": "一卡通公司2222" }], "parents": [{ "name": "大公司", "type": 0, "children": [{ "name": "发发委", "type": 0, "money": "780万元", "children": [{ "type": 0, "money": "780万元", "name": "123" }] }, { "name": "123发发委", "money": "780万元", "type": 0, "children": [{ "money": "780万元", "type": 0, "name": "123" }] }] }, { "name": "多多网", "money": "780万元", "type": 0, "children": [{ "type": 0, "money": "780万元", "name": "发哈哈" }] }, { "name": "龙龙投资", "money": "780万元", "type": 0, "children": [{ "type": 1, "money": "780万元", "name": "王林" }, { "type": 1, "money": "780万元", "name": "张峰" }, { "type": 1, "money": "780万元", "name": "侯明" }] }] },
            rootUp: '',
            rootDown: '',
            svg: ''
        }
    },

    mounted() {
        this.init()
    },

    methods: {
        init() {
            let d3 = this.d3
            let svgW = document.body.clientWidth
            let svgH = 500
            // 方块形状
            this.diamonds = {
                w: 145,
                h: 68,
                intervalW: 200,
                intervalH: 150
            }
            // 源头对象
            this.originDiamonds = {
                w: 190
            }
            this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1);
            // 主图
            this.svg = d3.select('#app').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
                .call(d3.zoom().scaleExtent([0, 5]).on('zoom', () => {
                    // 设置缩放位置以及平移初始位置
                    this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
                }))
                .attr('style', 'position: relative;z-index: 2;')
                .append('g').attr('id', 'g').attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')');
            let upTree = null
            let downTree = null
            // 拷贝树的数据
            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'
                }
            ]
            treeArr.map(item => {
                item.data.children.forEach(this.collapse);
                this.update(item.data, item.type, item.data)
            })
        },

        /*
         *[update 函数描述], [click 函数描述]
         *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
         *  @param  {[String]} showtype up表示向上 down表示向下
         *  @param  {[Object]} sourceTree 初始源对象
         */
        update(source, showtype, sourceTree) {
            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 * this.diamonds.intervalH;
            });

            let node = this.svg.selectAll('g.node' + showtype)
                .data(nodes, d => d.id || (d.id = showtype + ++this.i));

            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 + ')')

            // 创建矩形
            nodeEnter.append('rect')
                .attr('type', d => d.id)
                .attr('width', d => d.depth ? this.diamonds.w : this.originDiamonds.w)
                .attr('height', d => d.depth ? (d.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : 30)
                .attr('x', d => d.depth ? -this.diamonds.w / 2 : -this.originDiamonds.w / 2)
                .attr('y', d => d.depth ? showtype === 'up' ? -this.diamonds.h / 2 : 0 : -15)
                .attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#FD7D00' : '#7A9EFF')
                .attr('stroke-width', 1)
                .attr('rx', 5)
                .attr('ry', 5)
                .style('fill', d => {
                    if (d.data.type === COMPANY || !d.depth) {
                        return d._children ? '#FFF1D7' : (d.depth ? '#fff' : '#FD7D00')
                    } else if (d.data.type === PERSON) {
                        return d._children ? '#fff' : (d.depth ? '#fff' : '#7A9EFF')
                    }
                });

            // 创建圆 加减
            nodeEnter.append('circle')
                .attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
                .attr('r', (d) => d.depth ? (this.hasChildNodeArr.indexOf(d) === -1 ? 0 : SYMBOLA_S_R) : 0)
                .attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : this.diamonds.h : 0)
                .attr('cx', 0)
                .attr('fill', d => d.children ? '#fff' : '#FD7D00')
                .attr('stroke', d => d._children || d.children ? '#FD7D00' : '')
                .on('click', function (d) {
                    _this.click(d, showtype, sourceTree)
                    setTimeout(() => {
                        if (document.querySelector(`text[type="${d.id}"]`).innerHTML === '-') {
                            d.isOpen = false
                            this.innerHTML = '+'
                            this.setAttribute('fill', '#FD7D00')
                            document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#fff')
                            document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
                            document.querySelector(`text[type="${d.id}"]`).innerHTML = '+'
                        } else {
                            d.isOpen = true
                            this.setAttribute('fill', '#fff')
                            document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
                            document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
                            document.querySelector(`text[type="${d.id}"]`).innerHTML = '-'
                        }
                    }, DURATION)
                });

            // 持股比例
            nodeEnter.append('g')
                .attr('transform', () => 'translate(0,0)')
                .append('text')
                .attr('class', d => !d.depth ? 'proportion-hide' : 'proportion')
                .attr('x', d => d.x > 0 ? (showtype === 'up' ? -30 : 30) : 30)
                .attr('y', showtype === 'up' ? this.diamonds.h : -20)
                .attr('text-anchor', 'middle')
                .attr('fill', d => d.data.type === COMPANY ? '#FD7D00' : '#7A9EFF')
                .text(d => '30%');

            // 公司名称
            // y轴 否表源头的字体距离
            nodeEnter.append('text')
                .attr('class', 'text-style-name')
                .attr('x', 0)
                .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
                .attr('dy', d => d.depth ? (d.data.name.length > 9 ? '1.5em' : '2em') : '.3em')
                .attr('text-anchor', 'middle')
                .attr('fill', d => d.depth ? '#465166' : '#fff')
                .text(d => (d.data.name.length > 9) ? d.data.name.substr(0, 9) : d.data.name);

            // 名称过长 第二段
            nodeEnter.append('text')
                .attr('class', 'text-style-name')
                .attr('x', 0)
                .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
                .attr('dy', d => d.depth ? '3em' : '.3em')
                .attr('text-anchor', 'middle')
                .attr('fill', d => d.depth ? '#465166' : '#fff')
                .text(d => d.data.name.substr(9, d.data.name.length));

            // 认缴金额
            nodeEnter.append('text')
                .attr('class', 'text-style-money')
                .attr('x', 0)
                .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
                .attr('dy', d => d.data.name.substr(9, d.data.name.length).length ? '5em' : '4em')
                .attr('text-anchor', 'middle')
                .attr('fill', d => d.depth ? '#465166' : '#fff')
                .text(d => d.data.money);
            /*
            * 绘制箭头
            * @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' ? '-5' : '15')
                .attr('stroke-width', 2)
                .attr('fill', 'red')
                .append('path')
                .attr('d', 'M0,-5L10,0L0,5')
                .attr('fill', '#FD7D00');

            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' ? '-5' : '15')
                .attr('stroke-width', 2)
                .attr('fill', 'red')
                .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) + ')');

            // 代表是否展开的+-号,function this指向当前dom
            nodeEnter.append('svg:text')
                .attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
                .on('click', function (d) {
                    _this.click(d, showtype, sourceTree)
                    setTimeout(() => {
                        if (this.innerHTML === '-') {
                            d.isOpen = false
                            this.innerHTML = '+'
                            this.setAttribute('fill', '#fff')
                            document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
                            document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
                        } else {
                            d.isOpen = true
                            this.innerHTML = '-'
                            this.setAttribute('fill', '#FD7D00')
                            document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#fff')
                            document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
                        }
                    }, DURATION)
                })
                .attr('x', 0)
                .attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + this.diamonds.h / 2) : this.diamonds.h + 4) : 0)
                .attr('text-anchor', 'middle')
                .attr('fill', d => d._children ? '#fff' : '#FD7D00')
                .text(d => this.hasChildNodeArr.indexOf(d) !== -1 ? (source.depth && d.isOpen ? '-' : '+') : '');

            // 将退出节点转换到父节点的新位置.
            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.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 ? '#FD7D00' : '#7A9EFF')
                .style('fill-opacity', 1)
                .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);
            }
        },

        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;
                }
                this.update(source, showType, sourceTree)
            }
        },

        diagonal(s, d, showtype) {
            let path
            if (showtype === 'up') {
                path = `M ${s.x} ${-s.y + 24}
        C${s.x} -${(s.y + d.y) * 0.45},
         ${s.x} -${(s.y + d.y) * 0.45},
          ${d.x} -${d.y}`;
            } else {
                path = `M ${s.x} ${s.y}
        C${s.x} ${(s.y + d.y) * 0.45},
         ${s.x} ${(s.y + d.y) * 0.45},
          ${d.x} ${d.y}`;
            }
            return path;
        },

        saveImg() {
            alert('保存')
        },

        resetSvg() {
            this.d3.select('#treesvg').remove()
            this.init()
        }
    }

}
</script>
<style lang="scss">
.penetrate-chart {
    .bt-group {
        position: fixed;
        z-index: 999;
        right: 15px;
        bottom: 15px;
        button {
            width: 88px;
            height: 32px;
            display: block;
            border-radius: 18px;
            font-size: 14px;
            font-family: PingFangSC-Medium;
            font-weight: 500;
            line-height: 20px;
        }
        .save {
            background: rgba(255, 168, 9, 1);
            color: rgba(255, 255, 255, 1);
        }
        .reset {
            margin-top: 8px;
            color: rgba(255, 168, 9, 1);
            background: white;
            border: 1px solid rgba(255, 168, 9, 1);
        }
    }
}
#treesvg {
    display: block;
    margin: auto;
    #g {
        .linkup,
        .linkdown {
            fill: none;
            stroke-width: 1px;
        }
        .text-style-name {
            font-size: 12px; /*no*/
            font-family: PingFangSC-Medium;
            font-weight: 500;
        }
        .text-style-money {
            font-size: 10px; /*no*/
            font-family: PingFangSC-Regular;
            font-weight: 400;
            color: rgba(70, 81, 102, 1);
        }
        .proportion {
            font-size: 10px;
            font-family: PingFangSC-Regular;
            font-weight: 400;
        }
    }
    .proportion-hide,
    .hide-node {
        display: none;
    }
}
</style>


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342