(function() { d3.spatialsankey = function() { // Define control variables var spatialsankey = {}, map, nodes = {}, links = [], flows = [], node_flow_range = {}, link_flow_range = {}, remove_zero_links = true, remove_zero_nodes = true, version = '0.0.5'; spatialsankey.getColor = function(d) { var leakage = d; return 100 > leakage && leakage >= 87.5 ? '#d9534f' : 87.5 > leakage && leakage >= 75.0 ? '#e7908e' : 75.0 > leakage && leakage >= 62.5 ? '#f4cecd' : 62.5 > leakage && leakage >= 50.0 ? '#f9e2e2' : 50.0 > leakage && leakage >= 37.5 ? '#dbe9f5' : 37.5 > leakage && leakage >= 25.0 ? '#9fc5e5' : 25.0 > leakage && leakage >= 12.5 ? '#63a0d4' : 12.5 > leakage ? '#337ab7' : '#eee'; } function standardDeviation(values){ var avg = average(values); var squareDiffs = values.map(function(value){ var diff = value - avg; var sqrDiff = diff * diff; return sqrDiff; }); var avgSquareDiff = average(squareDiffs); var stdDev = Math.sqrt(avgSquareDiff); return stdDev; } function average(data){ var sum = data.reduce(function(sum, value){ return sum + value; }, 0); var avg = sum / data.length; return avg; } // Get or set leaflet map instance spatialsankey.lmap = function(_) { if(!arguments.length) return map; map = _; return spatialsankey; }; // Get or set data for nodes spatialsankey.nodes = function(_) { if (!arguments.length) return nodes; nodes = _; if(nodes.features) nodes = nodes.features; return spatialsankey; }; // Get or set data for flow volumes (optional) spatialsankey.flows = function(_) { if (!arguments.length) return flows; flows = _; return spatialsankey; }; // Calculates ranges for flows and nodes used for node radii and flow width drawing spatialsankey.ranges = function() { // Calculate aggregate flow values for nodes var createLinkIndex = function(direction1, direction2){ var linkIndex = links.reduce(function(output, link, index){ var flow = link.flow, targetNPI = link[direction1], sourceNPI = link[direction2], totalVisits = nodes[sourceNPI].totalvisits; if ( !(targetNPI in output) ) output[targetNPI] = {}; output[targetNPI][sourceNPI] = { "leakage": (1-(flow/totalVisits)), "value": flow, "marketshare": flow/totalVisits, "source": sourceNPI, "target": targetNPI }; return output; }, {}); return linkIndex }; var linksST = createLinkIndex('source', 'target'); var linksTS = createLinkIndex('target', 'source'); Object.keys(nodes).map(function(node){ if (nodes[node].type == 'target' || nodes[node].type == 'comp'){ nodes[node].aggregate_inflows = Object.keys(linksTS[node]).reduce(function(output, npi1, index){ return output += linksTS[node][npi1].value }, 0); nodes[node].aggregate_outflows = 0 } else if (nodes[node].type == 'source'){ nodes[node].aggregate_outflows = Object.keys(linksST[node]).reduce(function(output, npi2, index){ return output += linksST[node][npi2].value }, 0); nodes[node].aggregate_inflows = 0 } }); var arrayOfFlows = [] for (var v in links){ arrayOfFlows.push(links[v].flow) } // Calculate gobal ranges of values for links and nodes link_flow_range.min = d3.min(links, function(link) { return link.flow; }); link_flow_range.max = d3.max(links, function(link) { return link.flow; }); link_flow_range.std = standardDeviation(arrayOfFlows); node_flow_range.min = d3.min(Object.keys(nodes), function(node) { var inflow = nodes[node].aggregate_outflows; return inflow == 0 ? null : inflow; }); node_flow_range.max = d3.max(Object.keys(nodes), function(node) { return nodes[node].aggregate_outflows; }); return {links: link_flow_range, nodes: node_flow_range}; }; // Get or set data for links spatialsankey.links = function(_) { if (!arguments.length) return links; links = _; // Match nodes to links links = links.map(function(link){ // Get target and source features source_feature = nodes[link.source]; target_feature = nodes[link.target]; // If nodes were not found, return null if (!(source_feature && target_feature)) return null; // Set coordinates for source and target link.source_coords = source_feature.geometry.coordinates; link.target_coords = target_feature.geometry.coordinates; // If a flow for this link was specified, set flow value var flow = flows.filter(function(flow) { return flow.id == link.id; })[0]; if (flow) { link.flow = flow.flow; } // Make sure flow is a number link.flow = parseFloat(link.flow); return link; }); // Ignore links that have no node match var link_count = links.length; links = links.filter(function(link){ return link != null}); if(link_count != links.length){ console.log('Dropped ' + (link_count - links.length) + ' links that could not be matched to a node.'); } // Calculate ranges for dynamic drawing spatialsankey.ranges(); return spatialsankey; }; // Draw link element spatialsankey.link = function(options) { // Link styles // x and y shifts for control points var sx = 0.4, sy = 0.1; // With range of lines, set min and max to be equal for a constant width. var width_range = {min: 1, max: (1+link_flow_range.std/15)}; // If true, links are only shown if there is a flow value for them var hide_zero_flows = true; // Use arcs instead of S shaped bezier curves var arcs = false; // If true, lines are flipped along x axis var flip = false; // Customize link styles using options if(options){ if(options.xshift) sx = options.xshift; if(options.yshift) sy = options.yshift; if(options.minwidth) width_range.min = options.minwidth; if(options.maxwidth) width_range.max = options.maxwidth; if(options.use_arcs) arcs = options.use_arcs; if(options.flip) flip = options.flip; } // Define path drawing function var link = function(d) { // Set control point inputs var source = map.latLngToLayerPoint(d.source_coords), target = map.latLngToLayerPoint(d.target_coords), dx = source.x - target.x, dy = source.y - target.y; // Determine control point locations for different link styles if(!arcs){ if(dy < 0 || flip){ var controls = [sx*dx, sy*dy, sx*dx, sy*dy] } else { var controls = [sy*dx, sx*dy, sy*dx, sx*dy] } } else { if(dy < 0 || flip){ var controls = [sx*dx, sy*dy, sy*dx, sx*dy]; } else { var controls = [sy*dx, sx*dy, sx*dx, sy*dy]; } } return "M" + source.x + "," + source.y + "C" + (source.x - controls[0]) + "," + (source.y - controls[1]) + " " + (target.x + controls[2]) + "," + (target.y + controls[3]) + " " + target.x + "," + target.y; }; // Calculate width based on data range and width range setting var width = function(d) { // Don't draw flows with zero flow unless zero is the minimum if(d.flow == 0 && link_flow_range.min != 0) return 1; // Calculate width value based on flow range var diff = d.flow - link_flow_range.min, range = link_flow_range.max - link_flow_range.min; return (width_range.max - width_range.min)*(diff/range) + width_range.min; }; // Get or set link width function link.width = function(_) { if (!arguments.length) return width; width = _; return width; }; return link; }; // Draw node circle spatialsankey.node = function(options){ // Node styles // Range of node circles (set min and max equal for constant circle size) var node_radius_range = {min: 8, max: 8+(link_flow_range.std/40)}; // Range for color coding according to flow size (set colors for single coloring) node_color_range = ["yellow", "red"]; // Instantiate color scale function var color = d3.scale.linear() .domain([0, 1]) .range(node_color_range); // Customize link styles using options if(options){ if(options.minradius) node_radius_range.min = options.minradius; if(options.maxradius) node_radius_range.max = options.maxradius; if(options.mincolor) node_color_range[0] = options.mincolor; if(options.maxcolor) node_color_range[1] = options.maxcolor; } // Node object var node = {}; // Node object properties node.cx = function(d) { cx = map.latLngToLayerPoint(nodes[d].geometry.coordinates).x; if(!cx) return null; return cx; }; node.cy = function(d) { cy = map.latLngToLayerPoint(nodes[d].geometry.coordinates).y; if(!cy) return null; return cy; }; node.r = function(d) { // Math.sqrt(d.totalcharges) if (nodes[d].aggregate_outflows == 0) return 8; var diff = (0.003*(nodes[d].charges||1)) - node_flow_range.min, range = node_flow_range.max - node_flow_range.min; return (node_radius_range.max - node_radius_range.min)*(diff/range) + node_radius_range.min; }; node.color = function(_) { if (!arguments.length) return color; color = _; return node; }; node.fill = function(d, NPI) { // getColor from scale if node is source else purple var fill; if (nodes[d].type == 'source'){ fill = spatialsankey.getColor(nodes[d].leakage*100); } else { if (nodes[d].npi == NPI){ fill = '#2ca02c'; } else { fill = '#756bb1'; } } return fill; }; return node; }; return spatialsankey; }; })();