Overview
Rendering functions transform processed data into interactive visualizations using D3.js, vis-network, and Leaflet libraries.
Sankey Diagram
renderSankey
Renders an interactive Sankey diagram showing top-N flows between nodes with zoom and filtering capabilities.
Defined in app.js:1075-1335
Parameters
None - uses appState for configuration and data
Features
Interactive Zoom Pan and zoom with mouse/trackpad. Reset button available.
Node Filtering Click nodes to highlight connected flows and dim others.
Top-N Selection Show top 5-500 flows by weight with real-time updates.
Origin/Dest Filters Filter to specific origin or destination nodes.
Algorithm
Filter Edges
Apply origin/destination filters to aggregatedEdges
Sort & Slice
Sort by weight descending and take top-N edges
Extract Nodes
Collect unique nodes from filtered edges
Compute Layout
Use d3.sankey() to calculate node positions and link paths
Render SVG
Draw links, nodes, and labels with D3.js
Add Interactions
Attach zoom behavior and click handlers
Update Statistics
Display link counts and weight sums in UI
Configuration
Reads from appState:
sankeyTopN - Number of flows to display (default: 50)
sankeyOriginFilter - Origin node filter
sankeyDestFilter - Destination node filter
aggregatedEdges - Source data
Updates State
appState . sankeyStats = {
totalLinks: number , // Total edges in dataset
totalWeight: number , // Sum of all edge weights
displayedLinks: number , // Edges shown in diagram
displayedWeight: number // Sum of displayed edge weights
}
Visual Design
Links : Bezier curves with thickness proportional to weight
Nodes : Vertical bars with height proportional to total flow
Colors : d3.schemeCategory10 palette based on target node
Labels : Positioned left/right based on node position
Interaction Details
Node Click
Zoom Controls
Tooltips
// Click a node to highlight its connections
// - Selected node and connected nodes: opacity 1.0
// - Unconnected nodes: opacity 0.2
// - Connected links: opacity 0.8
// - Unconnected links: opacity 0.08
// Click again to deselect
// Mouse wheel: Zoom in/out
// Mouse drag: Pan the diagram
// + button: Zoom in 1.2x
// - button: Zoom out 0.833x
// Reset button: Return to initial view
// Hover over link:
// "Source → Target\n[weight] referrals"
// Hover over node:
// "Node Name\n[total flow] flujos"
Example Usage
// Render with default settings
renderSankey ();
// Update top-N and re-render
appState . sankeyTopN = 100 ;
renderSankey ();
// Filter to specific origin
appState . sankeyOriginFilter = "New York" ;
renderSankey ();
// Check statistics after rendering
console . log ( `Showing ${ appState . sankeyStats . displayedLinks } of ${ appState . sankeyStats . totalLinks } links` );
updateSankeyTopN(value) - Updates top-N setting (app.js:898)
updateSankeyFilters() - Updates filter settings (app.js:910)
sankeyZoom(scale) - Programmatic zoom control (app.js:925)
sankeyZoomReset() - Resets zoom to default (app.js:933)
Network Graph
renderNetwork
Renders an interactive force-directed network graph using vis-network with physics simulation.
Defined in app.js:1339-1614
Parameters
None - uses appState for configuration and data
Features
Force Layout Barnes-Hut physics simulation for natural node positioning
Neighborhood Highlighting Click nodes to highlight immediate neighbors
Node Sizing Node size proportional to total referral weight
Edge Scaling Edge thickness scaled by weight
Algorithm
Filter & Sort
Apply filters and sort edges by weight
Take Top-N
Select top-N edges (default: 100)
Build Node Map
Map node names to numeric IDs
Calculate Degrees
Sum incoming + outgoing weights for each node
Scale Sizes
Map degree values to visual sizes (15-100 radius)
Create Network
Initialize vis-network with data and physics options
Setup Interactions
Attach selection and highlight event handlers
Configuration
Reads from appState:
networkTopN - Number of edges to display (default: 100)
networkOriginFilter - Origin node filter
networkDestFilter - Destination node filter
aggregatedEdges - Source data
Updates State
appState . networkStats = {
totalLinks: number , // Total edges in dataset
totalWeight: number , // Sum of all edge weights
displayedLinks: number , // Edges shown in graph
displayedWeight: number // Sum of displayed edge weights
}
Physics Settings
const options = {
physics: {
enabled: true ,
barnesHut: {
gravitationalConstant: - 8000 ,
centralGravity: 0.5 ,
springLength: 250 ,
springConstant: 0.02 ,
damping: 0.95 ,
avoidOverlap: 0.2
},
stabilization: {
iterations: 200 ,
fit: true
}
}
}
Node Representation
Size Calculation
Color Assignment
Node Data
// Calculate total weight for each node (in + out)
const degree = {};
displayedEdges . forEach ( edge => {
degree [ fromId ] += edge . value ; // Outgoing
degree [ toId ] += edge . value ; // Incoming
});
// Scale to visual size (15 to 100)
const size = 15 + (( totalWeight - minDegree ) / degreeDelta ) * 85 ;
// Each node gets a unique color from d3.schemeCategory10
const colorScale = d3 . scaleOrdinal ( d3 . schemeCategory10 );
const color = colorScale ( nodeName );
{
id : numericId ,
label : nodeName ,
title : "NodeName - Referrals: 1,234" ,
size : calculatedSize ,
color : { background , border , highlight },
shape : 'dot'
}
Edge Representation
// Edge width scaled from 0.5 to 5
const width = 0.5 + (( edge . value - minEdgeValue ) / edgeDelta ) * 4.5 ;
{
id : edgeIndex ,
from : sourceId ,
to : targetId ,
value : weight ,
width : scaledWidth ,
title : "Source → Target<br/>Referrals: 1,234" ,
arrows : 'to' ,
color : { color : sourceColor , opacity : 0.6 },
smooth : { type : 'continuous' }
}
Interaction: Neighborhood Highlighting
Show View Highlighting Logic
const neighbourhoodHighlight = ( params ) => {
if ( params . nodes . length > 0 ) {
// A node was selected
const selectedNode = params . nodes [ 0 ];
const connectedNodes = network . getConnectedNodes ( selectedNode );
// Dim all nodes
allNodes . forEach ( node => {
node . color = "rgba(200,200,200,0.5)" ;
node . hiddenLabel = node . label ;
node . label = undefined ;
});
// Restore selected node and neighbors
[ selectedNode , ... connectedNodes ]. forEach ( nodeId => {
node . color = originalColor ;
node . label = node . hiddenLabel ;
});
} else {
// Restore all nodes
allNodes . forEach ( node => {
node . color = originalColor ;
node . label = node . hiddenLabel ;
});
}
};
Example Usage
// Render with default settings
renderNetwork ();
// Show more edges
appState . networkTopN = 200 ;
renderNetwork ();
// Filter to specific destination
appState . networkDestFilter = "Los Angeles" ;
renderNetwork ();
// Programmatic zoom
if ( window . networkInstance ) {
networkZoom ( 1.5 ); // Zoom in 1.5x
networkZoomReset (); // Fit to view
}
updateNetworkTopN(value) - Updates top-N setting (app.js:989)
updateNetworkFilters() - Updates filter settings (app.js:1001)
networkZoom(factor) - Zoom in/out (app.js:1016)
networkZoomReset() - Fit network to view (app.js:1023)
Ego Networks
updateEgoNetwork
function updateEgoNetwork ( panelId )
Renders a focused ego-network for a selected destination node, showing its immediate neighbors and connections.
Defined in app.js:591-875
Parameters
Panel identifier (1-4) corresponding to the ego network panel
Features
Shows 1-hop neighborhood of selected node
Includes edges between neighbors for context
Displays per-panel statistics (neighbors, edges, referrals)
Interactive drag with physics simulation
Automatic layout stabilization
Algorithm
Get Selection
Read selected destination from panel’s dropdown
Collect Edges
Get outgoing and incoming edges from indices
Find Neighbors
Extract all nodes connected to ego node
Include Context
Add edges between neighbors if they exist
Calculate Sizes
Size nodes by sum of edge weights
Create Network
Build vis-network with physics enabled
Stabilize Layout
Run physics simulation until stable, then disable
Node Selection
// Get ego node and its neighbors
const egoName = document . getElementById ( `ego ${ panelId } Dest` ). value ;
const outEdges = appState . outIndex . get ( egoName ) || [];
const inEdges = appState . inIndex . get ( egoName ) || [];
const neighbors = new Set ();
outEdges . forEach ( e => neighbors . add ( e . target ));
inEdges . forEach ( e => neighbors . add ( e . source ));
neighbors . add ( egoName ); // Include ego itself
Edge Inclusion
// Always include edges touching the ego node
const edgesToShow = [];
outEdges . forEach ( e => edgesToShow . push ( e ));
inEdges . forEach ( e => edgesToShow . push ( e ));
// Optionally include edges between neighbors
appState . aggregatedEdges . forEach ( e => {
if ( neighbors . has ( e . source ) && neighbors . has ( e . target )) {
if ( ! edgesToShow . includes ( e )) {
edgesToShow . push ( e );
}
}
});
Statistics Display
// Update panel statistics
const neighborCount = neighbors . size - 1 ; // Exclude ego
const edgesCount = edgesToShow . length ;
const totalWeight = edgesToShow . reduce (( s , e ) => s + e . value , 0 );
statsEl . textContent = neighborCount === edgesCount
? `Edges: ${ edgesCount } · Referrals: ${ totalWeight . toLocaleString () } `
: `Neighbors: ${ neighborCount } · Edges: ${ edgesCount } · Referrals: ${ totalWeight . toLocaleString () } ` ;
Physics Configuration
const options = {
physics: {
enabled: true ,
solver: 'barnesHut' ,
barnesHut: {
avoidOverlap: 0 ,
centralGravity: 0.25 ,
damping: 0.45 ,
gravitationalConstant: - 2800 ,
springConstant: 0.02 ,
springLength: 150
},
stabilization: {
enabled: true ,
fit: true ,
iterations: 1000
}
}
}
// Disable physics after stabilization
net . once ( 'stabilizationIterationsDone' , () => {
net . setOptions ({ physics: { enabled: false } });
net . fit ();
});
Drag Behavior
Show View Drag Physics Implementation
net . on ( 'dragStart' , ( params ) => {
// Enable physics temporarily
net . setOptions ({ physics: { enabled: true } });
// Apply "nudge" to connected nodes
const dragged = params . nodes [ 0 ];
const connected = net . getConnectedNodes ( dragged );
const positions = net . getPositions ([ dragged , ... connected ]);
connected . forEach ( nei => {
// Move neighbor slightly away from dragged node
const dx = positions [ nei ]. x - positions [ dragged ]. x ;
const dy = positions [ nei ]. y - positions [ dragged ]. y ;
const dist = Math . sqrt ( dx * dx + dy * dy ) || 1 ;
const nudgePx = 12 ;
net . moveNode ( nei ,
positions [ nei ]. x + ( dx / dist ) * nudgePx ,
positions [ nei ]. y + ( dy / dist ) * nudgePx
);
});
});
net . on ( 'dragEnd' , () => {
// Let physics settle for 1.2s, then disable
setTimeout (() => {
net . setOptions ({ physics: { enabled: false } });
}, 1200 );
});
Example Usage
// Attach to ego network selects
for ( let i = 1 ; i <= 4 ; i ++ ) {
const select = document . getElementById ( `ego ${ i } Dest` );
select . addEventListener ( 'change' , () => updateEgoNetwork ( i ));
}
// Programmatic update
const panel1Select = document . getElementById ( 'ego1Dest' );
panel1Select . value = "Chicago" ;
updateEgoNetwork ( 1 );
Geographic Map
renderMap
Renders an interactive geographic map with Leaflet showing nodes as markers and edges as lines.
Defined in app.js:1745-2160
Parameters
None - uses appState for configuration and data
Features
Shape Coding Circle = origin only, Square = destination only, Diamond = both
Color Coding Optional color-by-field with interactive legend
Cost Mode Toggle line width = distance × weight
Detailed Popups Click nodes/edges for breakdown tables with percentages
Algorithm
Extract Coordinates
Call extractNodeCoordinates() to populate coordinate map
Validate Data
Check that at least some nodes have valid coordinates
Initialize Map
Create or reuse Leaflet map instance
Apply Filters
Filter edges by origin, destination, and color group
Calculate Metrics
Compute edge metrics (weight or cost = distance × weight)
Draw Edges First
Render polylines below markers
Draw Nodes Second
Render shaped markers on top
Add Legends
Show color legend (if color-by) and shape legend
Update Statistics
Display georeferencing stats
Configuration
Reads from appState:
originLatCol, originLngCol - Origin coordinate columns
destLatCol, destLngCol - Destination coordinate columns
mapOriginFilter - Origin node filter
mapDestFilter - Destination node filter
mapColorCol - Column for color coding
mapLegendFilter - Active color group filter
mapCostMode - Enable cost mode (distance × weight)
Node Shapes
Circle (Origin)
Square (Destination)
Diamond (Both)
L . circleMarker ([ coords . lat , coords . lng ], {
radius: scaledRadius ,
fillColor: color ,
color: color ,
weight: 2 ,
opacity: 0.8 ,
fillOpacity: 0.7
})
const svg = `<svg width=" ${ d } " height=" ${ d } ">` +
`<rect x="1" y="1" width=" ${ d - 2 } " height=" ${ d - 2 } " ` +
`rx="2" fill=" ${ color } " fill-opacity="0.7" ` +
`stroke=" ${ color } " stroke-width="2"/>` +
`</svg>` ;
L . divIcon ({ html: svg , ... })
const svg = `<svg width=" ${ d } " height=" ${ d } ">` +
`<polygon points=" ${ d / 2 } ,0 ${ d } , ${ d / 2 } ${ d / 2 } , ${ d } 0, ${ d / 2 } " ` +
`fill=" ${ color } " fill-opacity="0.7" ` +
`stroke=" ${ color } " stroke-width="2"/>` +
`</svg>` ;
L . divIcon ({ html: svg , ... })
Edge Rendering
// Calculate distance using Haversine formula
function haversineDist ( lat1 , lng1 , lat2 , lng2 ) {
const R = 6371 ; // km
const toRad = v => v * Math . PI / 180 ;
const dLat = toRad ( lat2 - lat1 );
const dLng = toRad ( lng2 - lng1 );
const a = Math . sin ( dLat / 2 ) ** 2 +
Math . cos ( toRad ( lat1 )) * Math . cos ( toRad ( lat2 )) *
Math . sin ( dLng / 2 ) ** 2 ;
return R * 2 * Math . atan2 ( Math . sqrt ( a ), Math . sqrt ( 1 - a ));
}
// Calculate line width
const metric = useCost
? haversineDist ( from . lat , from . lng , to . lat , to . lng ) * edge . value
: edge . value ;
const lineWidth = useCost
? 2 + ( metric / maxMetric ) * 38
: 4 + ( edge . value / maxWeight ) * 36 ;
// Draw invisible hitbox (18px min) + visible line
L . polyline ( points , {
weight: Math . max ( lineWidth , 18 ),
opacity: 0 ,
interactive: true
}). bindPopup ( popupContent );
L . polyline ( points , {
color: edgeColor ,
weight: lineWidth ,
opacity: 0.5 ,
dashArray: '5, 5' ,
interactive: false
});
Show View Popup HTML Structure
let popupHtml = '' ;
// Outgoing connections (if node is origin)
if ( outEdges && outEdges . length > 0 ) {
const sorted = outEdges . sort (( a , b ) => b . value - a . value );
const totalSent = sorted . reduce (( s , e ) => s + e . value , 0 );
popupHtml += `<strong> ${ nodeName } </strong><br/>` +
`Total sent: ${ totalSent . toLocaleString () } <br/>` +
`<table>` +
`<tr><th>Dest</th><th>Weight</th><th>%</th></tr>` ;
sorted . forEach ( e => {
const pct = (( e . value / totalSent ) * 100 ). toFixed ( 1 );
popupHtml += `<tr>` +
`<td> ${ e . target } </td>` +
`<td> ${ e . value . toLocaleString () } </td>` +
`<td> ${ pct } %</td>` +
`</tr>` ;
});
popupHtml += `</table>` ;
}
// Incoming connections (if node is destination)
if ( inEdges && inEdges . length > 0 ) {
// Similar table for incoming edges
}
Color-by-Field
// Map nodes to color groups
const nodeColorMap = new Map ();
appState . rows . forEach ( row => {
const originNode = String ( row [ appState . originCol ] ?? '' ). trim ();
const colorValue = String ( row [ colorCol ] ?? '' ). trim ();
if ( originNode && ! nodeColorMap . has ( originNode )) {
nodeColorMap . set ( originNode , colorValue );
}
});
// Create color scale
const colorGroups = Array . from ( new Set ( nodeColorMap . values ())). sort ();
const colorScale = d3 . scaleOrdinal ()
. domain ( colorGroups )
. range ( colorPalette );
Interactive Legend
// Click legend item to filter to that group
legendRow . addEventListener ( 'click' , ( ev ) => {
appState . mapLegendFilter = isActive ? '' : groupName ;
renderMap (); // Re-render with filter
});
// When legend filter active:
// - Show only edges whose TARGET belongs to the group
// - Force all shown nodes to use the group's color
if ( legendFilter ) {
const groupNodes = new Set (
Array . from ( nodeColorMap . entries ())
. filter (([ n , g ]) => g === legendFilter )
. map (([ n ]) => n )
);
filteredEdges = filteredEdges . filter ( e => groupNodes . has ( e . target ));
}
Statistics
appState . mapStats = {
nodes: displayedNodes , // Markers rendered
displayedLinks: edgesDrawn , // Edges with valid coords
totalLinks: aggregatedEdges . length
};
const missingCoords = totalLinks - displayedLinks ;
const statsText = `Georeferenced: ${ displayedNodes } nodes · ` +
` ${ displayedLinks } / ${ totalLinks } links` +
( missingCoords > 0 ? ` ( ${ missingCoords } without coordinates)` : '' );
Example Usage
// Configure coordinate columns
appState . originLatCol = "Origin_Lat" ;
appState . originLngCol = "Origin_Lng" ;
appState . destLatCol = "Dest_Lat" ;
appState . destLngCol = "Dest_Lng" ;
// Render map
renderMap ();
// Enable cost mode
appState . mapCostMode = true ;
renderMap ();
// Color by region
appState . mapColorCol = "Region" ;
renderMap ();
// Filter to specific origin
appState . mapOriginFilter = "New York" ;
renderMap ();
// Check rendering results
console . log ( `Georeferenced ${ appState . mapStats . nodes } nodes` );
console . log ( `Drew ${ appState . mapStats . displayedLinks } of ${ appState . mapStats . totalLinks } links` );
extractNodeCoordinates() - Extracts lat/lng from data (app.js:1619)
updateMapFilters() - Updates filter settings (app.js:1724)
toggleMapCostMode() - Toggles cost mode (app.js:1738)
populateMapFilters() - Populates filter dropdowns (app.js:1684)
populateMapColorSelector() - Populates color-by dropdown (app.js:1713)
UI Helper Functions
switchTab
function switchTab ( tabName , sourceEl )
Switches the active visualization tab and shows corresponding controls.
Defined in app.js:405-425
Parameters
Name of tab to activate: ‘sankey’, ‘network’, ‘map’, or ‘ego’
Button element that triggered the tab switch (for styling)
showTabControls
function showTabControls ( tabName )
Shows visualization-specific controls in the toolbar.
Defined in app.js:430-550
Parameters
Name of active tab: ‘sankey’, ‘network’, ‘map’, or ‘ego’
Behavior
Dynamically generates toolbar HTML for the active visualization
Preserves previous filter values when switching back to a tab
Shows cached statistics if available
Hides toolbar for ego network tab (uses panel-specific controls)
showStatus
function showStatus ( message , type = 'info' )
Displays a status message in the application header.
Defined in app.js:566-573
Parameters
type
'info' | 'error' | 'warning'
default: "info"
Message type for styling
Large Datasets : Rendering functions use top-N filtering to maintain performance. For datasets with >10,000 edges:
Sankey: Limit to top 50-100 flows
Network: Limit to top 100-200 edges
Map: Filter by origin/destination for better performance
Ego Networks: Always bounded to 1-hop neighborhood
Rendering Pipeline