<div id="map-background"></div>
<svg width="1800" height="900"></svg>
<div class="slidecontainer">
  Authorship <input type="range" min="0" max="1" value="0.8" class="slider" id="myRange" step="0.02" /> Map
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://intervis-projects.ccs.neu.edu/ssvg/ssvg.js"></script>  
<script src="https://measure-fps.surge.sh/measureFPS.js"></script></script>
.links line {
        stroke: #999;
        stroke-opacity: 0.2;
    }
    .nodes circle {
        stroke: #fff;
        stroke-width: 1px;
    }
  .slidecontainer {
    position: absolute;
    top: 10px;
    right: 10px;
  }
  #map-background {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: -10;
    background: url('http://VISAuthorNodeLink.surge.sh/map.png') 6px 6px no-repeat;
  }
new SSVG({getFps: function(fps) {
          const fpsElement = document.getElementById("fps-element");
          fpsElement.innerHTML = fps;
      }});

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var projection = d3.geoMercator()
    .scale(250)
    .translate( [width / 2 - 50, height / 1.5]);

var path = d3.geoPath().projection(projection);

const geoPositionForce = (function() {
    const force = (alpha) => {
        if(force.savedNodes && force.savedNodes.length) {
            for(const node of force.savedNodes) {
                if(node.lat !== undefined) {
                    const target = projection([node.lng, node.lat]);
                    const difference = {
                        x: target[0] - node.x,
                        y: target[1] - node.y
                    };

                    node.x += difference.x * alpha * force.currentStrength;
                    node.y += difference.y * alpha * force.currentStrength;
                }
            }
        }
    };

    force.savedNodes = [];
    force.strencurrentStrengthgth = 1;

    force.initialize = (nodes) => {
        if(!nodes || !nodes.length) {
            return;
        }
        force.savedNodes = nodes;
    };

    force.strength = (strength) => {
        force.currentStrength = strength;
        return force;
    };

    return force;
});

const centeringForce = (function(cx, cy) {
    const force = (alpha) => {
        if(force.savedNodes && force.savedNodes.length) {
            for(const node of force.savedNodes) {
                const difference = {
                    x: cx - node.x,
                    y: cy - node.y
                };
                const differenceLength = Math.sqrt(Math.pow(difference.x, 2) + Math.pow(difference.y, 2));
                const normalizedDifference = {
                    x: difference.x / differenceLength,
                    y: difference.y / differenceLength
                };

                node.x += normalizedDifference.x * Math.pow(differenceLength, 1.4) * alpha * force.currentStrength;
                node.y += normalizedDifference.y * Math.pow(differenceLength, 1.4) * alpha * force.currentStrength;
            }
        }
    };

    force.savedNodes = [];
    force.strencurrentStrengthgth = 1;

    force.initialize = (nodes) => {
        if(!nodes || !nodes.length) {
            return;
        }
        force.savedNodes = nodes;
    };

    force.strength = (strength) => {
        force.currentStrength = strength;
        return force;
    };

    return force;
});

var diameter = 800;
var radius = diameter / 2;
var margin = 20;

function drawRing(graph) {
    
    var plot = svg.append("g")
        .attr("class", "plot")
        .attr("transform", "translate(" + width/2 + ", " + height/2 + ")");
   

    // calculate node positions
    ellipseLayout(graph.nodes);

    drawNodes(graph.nodes);
}

// Calculates node locations
function ellipseLayout(nodes) {
    // use to scale node index to theta value
    var scale = d3.scaleLinear()
        .domain([0, nodes.length])
        .range([0, 2 * Math.PI]);

    // calculate theta for each node
    nodes.forEach(function(d, i) {
        // calculate polar coordinates
        var theta  = scale(i);
        var radial = radius - margin;

        // convert to cartesian coordinates
        d.x = (radial + 500) * Math.sin(theta);
        d.y = (radial + 65) * Math.cos(theta);
    });
}

// Draws nodes with tooltips
function drawNodes(nodes) {
    d3.select(".plot").selectAll(".node")
        .data(nodes)
        .enter()
        .append("circle")
        .attr("class", "node")
        .attr("id", function(d, i) { return d.id; })
        .attr("cx", function(d, i) { return d.x; })
        .attr("cy", function(d, i) { return d.y; })
        .attr("r", 2)
        .style("fill",   function(d, i) { return 'red' });
}

    d3.csv('https://ssvg-example-resources.surge.sh/affiliationList.csv', function(err, affiliations) {

        const affiliationsWithLocation = affiliations
            .filter(a => a.Position);
        for(const affiliation of affiliationsWithLocation) {
            const splitPos = affiliation.Position.split(',');
            affiliation.lat = parseFloat(splitPos[0]);
            affiliation.lng = parseFloat(splitPos[1]);
        }
        const affiliationsByName = {};
        for(const affiliation of affiliations) {
            affiliationsByName[affiliation.Name] = affiliation;
        }

        svg.append('g')
            .attr('class', 'affiliations')
            .selectAll('circle')
            .data(affiliationsWithLocation)
            .enter().append('circle')
            /*.attr('r', 5)
            .attr('cx', d => projection([d.lng, d.lat])[0])
            .attr('cy', d => projection([d.lng, d.lat])[1])
            .style('fill', () => '#a00');*/

        var color = d3.scaleOrdinal(d3.schemeCategory20);

      const forceMapStrengths = {
        link: 0,
        charge: 0,
        radial: 0,
        geoPosition: 1,
        mapOpacity: 1,
        center: 0
      };

      const forceAuthorshipStrengths = {
        link: 0.6,
        charge: -30,
        radial: 0,//0.07,
        geoPosition: 0,
        mapOpacity: 0,
        center: 0.015
      };

      function forceSelection(mapPortion){
          return {
            link: forceMapStrengths.link * mapPortion + forceAuthorshipStrengths.link * (1 - mapPortion),
            charge: forceMapStrengths.charge * mapPortion + forceAuthorshipStrengths.charge * (1 - mapPortion),
            radial: forceMapStrengths.radial * mapPortion + forceAuthorshipStrengths.radial * (1 - mapPortion),
            geoPosition: forceMapStrengths.geoPosition * mapPortion + forceAuthorshipStrengths.geoPosition * (1 - mapPortion),
            mapOpacity: forceMapStrengths.mapOpacity * mapPortion + forceAuthorshipStrengths.mapOpacity * (1 - mapPortion),
            center: forceMapStrengths.center * mapPortion + forceAuthorshipStrengths.center * (1 - mapPortion),
          }
      }
        
        
        var slider = document.getElementById("myRange");
        var forceSettings = forceSelection(slider.value);
        
        slider.oninput = function() {
            simulation.alpha(1).restart();
            forceSettings = forceSelection(this.value);
            applyStrengths(forceSettings);

            document.getElementById('map-background').style.opacity = forceSettings.mapOpacity;
        };

        var simulation = d3.forceSimulation().alphaDecay(0.02);

        const forceLink = d3.forceLink().id(function(d) { return d.id; });
        const forceManyBody = d3.forceManyBody();
        const forceCollide = d3.forceCollide().radius(function(d) {
            return Math.sqrt(30 * d.paperIndex.length)
        });
        const forceRadial = d3.forceRadial(300, width / 2, height / 2);
        const forceGeo = geoPositionForce();
        const forceCenter = centeringForce(width / 2, height / 2);

        function applyStrengths(strength) {
            simulation.force("link", forceLink.strength(strength.link))
                .force("charge", forceManyBody.strength(strength.charge))
                .force('collision', forceCollide)
                .force("radial", forceRadial.strength(strength.radial))
                .force("center", forceCenter.strength(strength.center))
                .force('geoposition', forceGeo.strength(strength.geoPosition));
        }
        applyStrengths(forceSettings);

        d3.json("https://ssvg-example-resources.surge.sh/VisAuthors10.json", function(error, graph) {

            console.log(affiliationsWithLocation);
            if (error) throw error;

            for(const node of graph.nodes) {
                const match = affiliationsWithLocation.filter(a => a.Name === node.affiliation);
                if(match && match.length > 0) {
                    node.lat = match[0].lat;
                    node.lng = match[0].lng;
                    node.x = projection([node.lng, node.lat])[0];
                    node.y = projection([node.lng, node.lat])[1];
                }
            }

            graph.nodes.sort((a,b) => a.paperIndex.length - b.paperIndex.length);

            var link = svg.append("g")
                .attr("class", "links")
                .selectAll("line")
                .data(graph.links)
                .enter().append("line")
                .attr("stroke-width", function(d) { return Math.sqrt(d.value); });
            var node = svg.append("g")
                .attr("class", "nodes")
                .selectAll("g")
                .data(graph.nodes)
                .enter().append("g");

            var circle = node.append('circle')
                .attr("r", d => Math.sqrt(20 * d.paperIndex.length))
                .attr("fill", function(d) {
                    const affiliation = affiliationsByName[d.affiliation];
                    if(!affiliation) {
                        //console.log(d.affiliation, affiliationsByName);
                        return '#aaa';
                    }
                    const name = affiliation.NameSimple ? affiliation.NameSimple : d.affiliation;
                    return color(name);
                })
                .call(d3.drag()
                    .on("start", dragstarted)
                    .on("drag", dragged)
                    .on("end", dragended));
            circle.append("title")
                .text(function(d) { return d.id + ', ' + d.affiliation; });

            node.append('text')
                .attr('text-anchor', 'middle')
                .attr('dy', 5)
                .attr('font-size', 16)
                .attr('pointer-events', 'none')
                .text(d => {
                    const nameParts = d.id.split(' ');
                    const initials = nameParts[0][0] + nameParts[nameParts.length - 1][0];
                    return d.paperIndex.length >= 8 ? initials : '';
                });

            simulation
                .nodes(graph.nodes)
                .on("tick", ticked);
            simulation.force("link")
                .links(graph.links);

            function ticked() {
                link
                    .attr("x1", function(d) { return d.source.x; })
                    .attr("y1", function(d) { return d.source.y; })
                    .attr("x2", function(d) { return d.target.x; })
                    .attr("y2", function(d) { return d.target.y; });

                svg.select('.nodes').selectAll('circle')
                    .attr('cx', d => d.x)
                    .attr('cy', d => d.y);

                svg.selectAll('text')
                        .attr('x', d => d.x)
                        .attr('y', d => d.y);
            }
        });
        function dragstarted(d) {
            if (!d3.event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }
        function dragged(d) {
            d.fx = d3.event.x;
            d.fy = d3.event.y;
        }
        function dragended(d) {
            if (!d3.event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }
    });

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.