Source

modules/d3/charts/BubbleDrag.chart.js

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

/**
 * @memberOf D3Module
 * @function
 * @name bubbleDragChart
 * @desc function for create a bubble drag 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>[
 *       {
 *          id: <String> "A",
 *           value: <number> 150,
 *           group: <number> 1,
 *           properties?: <object> {
 *               opacity?: <number> 0.23,
 *               border_width?: <number> 5,
 *               background_color?: <String> '#000',
 *               border_color?: <String> '#523'
 *           },
 *           icon?: <String> '.../.../.../.png',
 *           description?: <String> 'Text'
 *       }, ...
 * ]</code>
 * @param {number=} [width=500] - chart width inside the container
 * @param {number=} [height=500] - chart height inside the container
 * @param {number=} [relativeRadius=null] - relative size radius to the circles length
 * @param {string=} [backgroundColor='white'] - background color for the chart
 * @param {function=} [onClickFunctionCallback=() => {}] - function callback to onClick event,
 * with parameter d = node of data or node selected, this node contains attributes of data[index] +
 * attributes of html element
 * @see <img src="https://i.imgur.com/BMPwD9E.png"></img>
 * @example D3.bubbleDragChart(
 *     document.getElementById('charts_container'),
 *     'bubble_drag_chart',
 *     [
 *       {
 *         id: "A",
 *         value: 150,
 *         group: 1,
 *         properties: {
 *             opacity: 0.23,
 *             border_width: 5,
 *             background_color: '#000',
 *             border_color: '#523'
 *         },
 *         icon: 'assets/img/exito.png',
 *         description: "Exito movil"
 *       },
 *       {
 *         id: "B",
 *         value: 20,
 *         group: 2,
 *         icon: 'assets/img/claro.png'
 *       },
 *       {
 *         id: "C",
 *         value: 20,
 *         group: 3
 *       },
 *       {
 *         id: "D",
 *         value: 20,
 *         group: 1
 *       },
 *       {
 *         id: "E",
 *         value: 20,
 *         group: 1
 *       },
 *               {
 *         id: "F",
 *         value: 20,
 *         group: 3
 *       },
 *       {
 *         id: "G",
 *         value: 20,
 *         group: 1
 *       },
 *       {
 *         id: "H",
 *         value: 20,
 *         group: 4
 *       }
 *    ]
 * );
 */
module.exports = (
    htmlElementContainer,
    idElement,
    data,
    width = 500,
    height = 500,
    relativeRadius = undefined,
    backgroundColor = 'white',
    onClickFunctionCallback = () => {}
) => {

    const svg = d3.create('svg')
        .attr('width', width)
        .attr('height', height)
        .style('background-color', backgroundColor);

    const centerX = width / 50;
    const centerY = height / 50;
    const strength = 0.05;
    const scale = d3.scaleOrdinal(d3.schemeCategory10);

    const dragStart = (d) => {
        if (!d3.event.active) {
            simulation.alphaTarget(.5).restart();
        }
        d.fx = d.x;
        d.fy = d.y;
    };

    const drag = (d) => {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
    };

    const dragEnd = (d) => {
        if (!d3.event.active) {
            simulation.alphaTarget(.003);
        }
        d.fx = null;
        d.fy = null;
    };

    const validateProperties = (d, property, result) => {
        if (d.properties && d.properties[property]) return d.properties[property]
        return result
    }

    const ticked = () => {
        node
            .attr('transform', d => `translate(${d.x},${d.y})`)
            .select('.node')
            .attr('r', d => d.r);
    };

    const node = svg.selectAll('.node')
        .data(data)
        .enter()
        .append('g')
        .attr('class', 'node')
        .call(d3.drag()
            .on('start', dragStart)
            .on('drag', drag)
            .on('end', dragEnd))
        .on('click', d => onClickFunctionCallback(d));

    node.append('circle')
        .attr('r', d => {
            d.r = relativeRadius !== undefined ? (d.value / relativeRadius) : d.value;
            return d.r;
        })
        .attr('cx', width / 2)
        .attr('cy', height / 2)
        .style('fill', d => validateProperties(d, 'background_color', scale(d.group)))
        .style('fill-opacity', d => validateProperties(d, 'opacity', 1))
        .attr('stroke', d => validateProperties(d, 'border_color', scale(d.group)))
        .style('stroke-width', d => validateProperties(d, 'border_width', 1));

    node.append('title')
        .text(d => d.description ? `id: ${d.id}\ndescription: ${d.description}` : `id: ${d.id}`);

    node.filter(d => String(d.icon).includes('img/'))
        .append('image')
        .classed('node-icon', true)
        .attr('clip-path', d => `url(#clip-${d.id})`)
        .attr('xlink:href', d => d.icon)
        .attr('x', d => (width / 2) - d.r * 0.7)
        .attr('y', d => ((height / 2) - d.r * 0.7))
        .attr('height', d => d.r * 2 * 0.7)
        .attr('width', d => d.r * 2 * 0.7);

    const simulation = d3.forceSimulation()
        .force('charge', d3.forceManyBody())
        .force('collide', d3.forceCollide(d => d.r + 1))
        .force('x', d3.forceX(centerX).strength(strength))
        .force('y', d3.forceY(centerY).strength(strength));

    simulation.nodes(data).on('tick', ticked);

    appendChart(svg, idElement, htmlElementContainer)
};