Source

modules/d3/charts/directionalGraph.chart.js

const {
    d3,
    drag,
    color,
    addTitleNode,
    addTitleArch,
    addNodeFunctions,
    addArchFunctions,
    appendChart
} = require('../others/d3.graphCharts.utils');

/**
 * @memberOf D3Module
 * @function
 * @name directionalGraphChart
 * @desc function for create a graph 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>{
 *     nodes: [{ id: string, group: number }],
 *     arches: [{ source: string, target: string, type: string, ... }]
 * }</code>
 * @param {boolean=} [showIds=false] - it'll show id for each node
 * @param {number=} [spaceOfLegend=[]] - space between legends
 * @param {array=} [nodeFunctions=[]] - functions of each node within the chart, with the structure:
 * <code>nodeFunctions: [{ event: string // event type, handler: function(node?) // action to take }]</code>
 * @param {array=} [archesFunctions=null] - functions of each arch within the chart, with the structure:
 * <code>archesFunctions: [{ event: string // event type, handler: function(arch?) // action to take }]</code>
 * @param {number=} [width=500] - chart width inside the container
 * @param {number=} [height=500] - chart height inside the container
 * @param {string=} [backgroundColor='white'] - background color for the chart
 * @see <img src="https://i.imgur.com/hZxqGSc.jpg"></img>
 * @example
 * D3.directionalGraphChart(
 *   document.getElementById('charts'),
 *   'chart',
 *   {
 *     nodes: [
 *       { id: "Microsoft", group: 1 },
 *       { id: "Amazon", group: 2 },
 *       { id: "HTC", group: 1 },
 *       { id: "Samsung", group: 2 },
 *       { id: "Apple", group: 1 },
 *       { id: "Motorola", group: 1 },
 *       { id: "Nokia", group: 2 },
 *       { id: "Kodak", group: 2 },
 *       { id: "Barnes & Noble", group: 4 },
 *       { id: "Foxconn", group: 1 },
 *       { id: "Oracle", group: 2 },
 *       { id: "Google", group: 1 },
 *       { id: "Inventec", group: 5 },
 *       { id: "LG", group: 1 },
 *       { id: "RIM", group: 1 },
 *       { id: "Sony", group: 3 },
 *       { id: "Qualcomm", group: 3 },
 *       { id: "Huawei", group: 3 },
 *       { id: "ZTE", group: 1 },
 *       { id: "Ericsson", group: 1 }
 *     ],
 *     arches: [
 *       {
 *         source: "Microsoft",
 *         target: "Amazon",
 *         type: "licensing"
 *       }, {
 *         source: "Microsoft",
 *         target: "HTC",
 *         type: "licensing"
 *       }, {
 *         source: "Samsung",
 *         target: "Apple",
 *         type: "suit"
 *       }, {
 *         source: "Motorola",
 *         target: "Apple",
 *         type: "suit"
 *       }, {
 *         source: "Nokia",
 *         target: "Apple",
 *         type: "resolved"
 *       }, {
 *         source: "HTC",
 *         target: "Apple",
 *         type: "suit"
 *       }, {
 *         source: "Kodak",
 *         target: "Apple",
 *         type: "suit"
 *       }, {
 *         source: "Microsoft",
 *         target: "Barnes & Noble",
 *         type: "suit"
 *       }, {
 *         source: "Microsoft",
 *         target: "Foxconn",
 *         type: "suit"
 *       }, {
 *         source: "Oracle",
 *         target: "Google",
 *         type: "suit"
 *       }, {
 *         source: "Apple",
 *         target: "HTC",
 *         type: "suit"
 *       }, {
 *         source: "Microsoft",
 *         target: "Inventec",
 *         type: "suit"
 *       }, {
 *         source: "Samsung",
 *         target: "Kodak",
 *         type: "resolved"
 *       }, {
 *         source: "LG",
 *         target: "Kodak",
 *         type: "resolved"
 *       }, {
 *         source: "RIM",
 *         target: "Kodak",
 *         type: "suit"
 *       }, {
 *         source: "Sony",
 *         target: "LG",
 *         type: "suit"
 *       }, {
 *         source: "Kodak",
 *         target: "LG",
 *         type: "resolved"
 *       }, {
 *         source: "Apple",
 *         target: "Nokia",
 *         type: "resolved"
 *       }, {
 *         source: "Qualcomm",
 *         target: "Nokia",
 *         type: "resolved"
 *       }, {
 *         source: "Apple",
 *         target: "Motorola",
 *         type: "suit"
 *       }, {
 *         source: "Microsoft",
 *         target: "Motorola",
 *         type: "suit"
 *       }, {
 *         source: "Motorola",
 *         target: "Microsoft",
 *         type: "suit"
 *       }, {
 *         source: "Huawei",
 *         target: "ZTE",
 *         type: "suit"
 *       }, {
 *         source: "Ericsson",
 *         target: "ZTE",
 *         type: "suit"
 *       }, {
 *         source: "Kodak",
 *         target: "Samsung",
 *         type: "resolved"
 *       }, {
 *         source: "Apple",
 *         target: "Samsung",
 *         type: "suit"
 *       }, {
 *         source: "Kodak",
 *         target: "RIM",
 *         type: "suit"
 *       }, {
 *         source: "Nokia",
 *         target: "Qualcomm",
 *         type: "suit"
 *       }
 *     ]
 *   },
 *   false,
 *   [
 *     {
 *       event: 'click',
 *       handler: console.log
 *     }
 *   ],
 *   [
 *     {
 *       event: 'click',
 *       handler: console.log
 *     }
 *   ]
 *   );
 */
module.exports = (
    htmlElementContainer,
    idElement,
    data,
    showIds = false,
    nodeFunctions = [],
    archesFunctions = [],
    spaceOfLegend = undefined,
    width = 500,
    height = 500,
    backgroundColor = 'white'
) => {
    const links = data.arches.map(d => Object.create(d));
    const nodes = data.nodes.map(d => Object.create(d));
    const types = Array.from(new Set(links.map(d => d.type)));
    const scale = d3.scaleOrdinal(types, [
        '#7fc97f',
        '#beaed4',
        '#86cdfd',
        '#ffff99',
        '#386cb0',
        '#f0027f',
        '#bf5b17',
        '#666666'
    ]);

    const simulation = d3.forceSimulation(nodes)
        .force('link', d3.forceLink(links).id(d => d.id))
        .force('charge', d3.forceManyBody().strength(-400))
        .force('x', d3.forceX())
        .force('y', d3.forceY());

    const svg = d3.create('svg')
        .attr('height', height)
        .attr('width', width)
        .style('background-color', backgroundColor)
        .attr('viewBox', [-width / 2, -height / 2, width, height]);

    const linkArc = (d) => {
        const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);

        return `
            M${d.source.x},${d.source.y}
            A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
            `;
    }

    svg.append('defs').selectAll('marker')
        .data(types)
        .join('marker')
        .attr('id', d => `arrow-${d}`)
        .attr('viewBox', '0 -5 10 10')
        .attr('refX', 15)
        .attr('refY', -0.5)
        .attr('markerWidth', 6)
        .attr('markerHeight', 6)
        .attr('orient', 'auto')
        .append('path')
        .attr('fill', scale)
        .attr('d', 'M0,-5L10,0L0,5');

    let auxPositionLegend = 0;
    const typeSetSVG = [];

    const link = svg.append('g')
        .attr('fill', 'none')
        .attr('stroke-width', 1.3)
        .selectAll('path')
        .data(links)
        .join('path')
        .attr('stroke', d => {
            const colorScale = scale(d.type);
            const posX = -((width / 2) - 50);
            const posY = -((height / 2) - 50);
            const validateFindType = typeSetSVG.includes(d.type);

            if(!validateFindType) {
                svg.append('rect')
                    .attr('x', posX)
                    .attr('y', posY + auxPositionLegend)
                    .attr("width", 12)
                    .attr("height", 12)
                    .style('fill', colorScale);

                svg.append('text')
                    .attr('x', posX + 20)
                    .attr('y', posY + auxPositionLegend + 7)
                    .text(d.type)
                    .style('font-size', '15px')
                    .attr('alignment-baseline','middle');

                typeSetSVG.push(d.type);
            }

            auxPositionLegend += spaceOfLegend ? spaceOfLegend : ( ( - ( posX + posY ) / 10 ) / 5 );

            return colorScale;
        })
        .attr('marker-end', d => `url(${new URL(`#arrow-${d.type}`, location)})`);

    const node = svg.append("g")
        .attr("fill", "currentColor")
        .attr("stroke-linecap", "round")
        .attr("stroke-linejoin", "round")
        .selectAll("g")
        .data(nodes)
        .join("g")
        .attr('fill', color(nodes))
        .call(drag(simulation));

    if (showIds) {
        node.append("text")
            .attr("x", 8)
            .attr("y", "0.31em")
            .text(d => d.id)
            .clone(true).lower()
            .attr("stroke", "white")
            .attr("stroke-width", 3);
    }

    addTitleArch(link, data);
    addTitleNode(node);

    node.append('circle')
        .attr('stroke', 'white')
        .attr('stroke-width', 1.5)
        .attr('r', 5);

    addNodeFunctions(nodeFunctions, node, data);
    addArchFunctions(archesFunctions, link, data);

    simulation.on('tick', () => {
        link.attr('d', linkArc);
        node.attr('transform', d => `translate(${d.x},${d.y})`);
    });

    appendChart(svg, idElement, htmlElementContainer);
}