Source

modules/d3/charts/collapsableTree.chart.js

const {
    appendChart,
    d3
} = require('../others/d3.utils');

/**
 * @memberOf D3Module
 * @function
 * @name collapsableTreeChart
 * @desc function for create a collapsable tree chart
 * @param {HTMLBodyElement} htmlElementContainer - container html element, where the chart is inserted
 * @param {string} idElement - chart id
 * @param {object} data - data to be plotted within the chart, with the structure:
 * <code>{ name: string, children: array[data] }</code>
 * @param {number=} [width=1050] - chart width inside the container
 * @param {number=} [height=300] - chart height inside the container
 * @param {Object=} [margin={ top: 10, right: 120, bottom: 10, left: 120 }] - view box container
 * @param {string=} [backgroundColor='white'] - background color for the chart
 * @see <img src="https://i.imgur.com/cOzfwr9.jpg"></img>
 * @example D3.collapsableTreeChart(
 *    document.getElementById('charts'),
 *    'collapsable_tree_chart',
 *    {
 *     name: 'root',
 *     children: [
 *       {
 *         name: 'node_1',
 *         children: [
 *           {
 *             name: 'node_1.1',
 *             children: [
 *               {
 *                 name: 'node_1.1.1'
 *               },
 *               {
 *                 name: 'node_1.1.2'
 *               },
 *               {
 *                 name: 'node_1.1.3'
 *               }
 *             ]
 *           }
 *         ]
 *       },
 *       {
 *         name: 'node_2',
 *         children: [
 *           {
 *             name: 'node_2.1',
 *             children: [
 *               {
 *                       name: 'node_2.1.1'
 *               },
 *               {
 *                 name: 'node_2.1.2'
 *               },
 *               {
 *                 name: 'node_2.1.3',
 *                 children: [
 *                   {
 *                     name: 'node_2.1.3.1',
 *                     children: [
 *                       {
 *                         name: 'node_2.1.3.1.1'
 *                       },
 *                       {
 *                         name: 'node_2.1.3.1.2'
 *                       },
 *                       {
 *                         name: 'node_2.1.3.1.3'
 *                       }
 *                     ]
 *                   }
 *                 ]
 *               }
 *             ]
 *           },
 *           {
 *             name: 'node_2.2'
 *           },
 *           {
 *             name: 'node_2.3'
 *           },
 *         ]
 *       }
 *     ]
 *   }
 * );
 */
module.exports = (
    htmlElementContainer,
    idElement,
    data,
    width = 1050,
    height = 300,
    margin = { top: 10, right: 120, bottom: 10, left: 120 },
    backgroundColor = 'white'
) => {
    const root = d3.hierarchy(data);
    const dx = 10, dy = 159;
    const diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x);
    const tree = d3.tree().nodeSize([dx, dy]);

    root.x0 = dy / 2;
    root.y0 = 0;
    root.descendants().forEach((d, i) => {
        d.id = i;
        d._children = d.children;
        if (d.depth && d.data.name.length !== 7) d.children = null;
    });

    const svg = d3.create('svg');

    svg.attr('height', height);
    svg.attr('width', width);
    svg.style('background-color', backgroundColor);

    const gLink = svg.append('g')
        .attr('fill', 'none')
        .attr('stroke', '#555')
        .attr('stroke-opacity', 0.4)
        .attr('stroke-width', 1.5);

    const gNode = svg.append('g')
        .attr('cursor', 'pointer')
        .attr('pointer-events', 'all');

    const update = (source) => {
        const duration = d3.event && d3.event.altKey ? 2500 : 250;
        const nodes = root.descendants().reverse();
        const links = root.links();

        tree(root);

        let left = root;
        let right = root;

        root.eachBefore(node => {
            if (node.x < left.x) left = node;
            if (node.x > right.x) right = node;
        });

        const height1 = right.x - left.x + margin.top + margin.bottom;

        const transition = svg.transition()
            .duration(duration)
            .attr('viewBox', [-margin.left, left.x - margin.top, width, height1])
            .tween('resize', window.ResizeObserver ? null : () => svg.dispatch('toggle'));

        const node = gNode.selectAll('g')
            .data(nodes, d => d.id);

        const nodeEnter = node.enter().append('g')
            .attr('transform', () => `translate(${source.y0},${source.x0})`)
            .attr('fill-opacity', 0)
            .attr('stroke-opacity', 0)
            .on('click', d => {
                d.children = d.children ? null : d._children;
                update(d);
            });

        nodeEnter.append('circle')
            .attr('r', 2.5)
            .attr('fill', d => d._children ? '#555' : '#999')
            .attr('stroke-width', 10);

        nodeEnter.append('text')
            .attr('dy', '0.31em')
            .attr('x', d => d._children ? -6 : 6)
            .attr('text-anchor', d => d._children ? 'end' : 'start')
            .text(d => d.data.name)
            .clone(true).lower()
            .attr('stroke-linejoin', 'round')
            .attr('stroke-width', 3)
            .attr('stroke', 'white');

        node.merge(nodeEnter).transition(transition)
            .attr('transform', d => `translate(${d.y},${d.x})`)
            .attr('fill-opacity', 1)
            .attr('stroke-opacity', 1);

        node.exit().transition(transition).remove()
            .attr('transform', () => `translate(${source.y},${source.x})`)
            .attr('fill-opacity', 0)
            .attr('stroke-opacity', 0);

        const link = gLink.selectAll('path')
            .data(links, d => d.target.id);

        const linkEnter = link.enter().append('path')
            .attr('d', () => {
                const o = { x: source.x0, y: source.y0 };
                return diagonal({ source: o, target: o });
            });

        link.merge(linkEnter).transition(transition)
            .attr('d', diagonal);

        link.exit().transition(transition).remove()
            .attr('d', () => {
                const o = { x: source.x, y: source.y };
                return diagonal({ source: o, target: o });
            });

        root.eachBefore(d => {
            d.x0 = d.x;
            d.y0 = d.y;
        });
    };

    update(root);
    appendChart(svg, idElement, htmlElementContainer);
}