Skip to main content

Overview

Rendering functions transform processed data into interactive visualizations using D3.js, vis-network, and Leaflet libraries.

Sankey Diagram

renderSankey

function 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

1

Filter Edges

Apply origin/destination filters to aggregatedEdges
2

Sort & Slice

Sort by weight descending and take top-N edges
3

Extract Nodes

Collect unique nodes from filtered edges
4

Compute Layout

Use d3.sankey() to calculate node positions and link paths
5

Render SVG

Draw links, nodes, and labels with D3.js
6

Add Interactions

Attach zoom behavior and click handlers
7

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

// 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

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

function 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

1

Filter & Sort

Apply filters and sort edges by weight
2

Take Top-N

Select top-N edges (default: 100)
3

Build Node Map

Map node names to numeric IDs
4

Calculate Degrees

Sum incoming + outgoing weights for each node
5

Scale Sizes

Map degree values to visual sizes (15-100 radius)
6

Create Network

Initialize vis-network with data and physics options
7

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

// 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;

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

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

panelId
number
required
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

1

Get Selection

Read selected destination from panel’s dropdown
2

Collect Edges

Get outgoing and incoming edges from indices
3

Find Neighbors

Extract all nodes connected to ego node
4

Include Context

Add edges between neighbors if they exist
5

Calculate Sizes

Size nodes by sum of edge weights
6

Create Network

Build vis-network with physics enabled
7

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));

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

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

function 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

1

Extract Coordinates

Call extractNodeCoordinates() to populate coordinate map
2

Validate Data

Check that at least some nodes have valid coordinates
3

Initialize Map

Create or reuse Leaflet map instance
4

Apply Filters

Filter edges by origin, destination, and color group
5

Calculate Metrics

Compute edge metrics (weight or cost = distance × weight)
6

Draw Edges First

Render polylines below markers
7

Draw Nodes Second

Render shaped markers on top
8

Add Legends

Show color legend (if color-by) and shape legend
9

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

L.circleMarker([coords.lat, coords.lng], {
  radius: scaledRadius,
  fillColor: color,
  color: color,
  weight: 2,
  opacity: 0.8,
  fillOpacity: 0.7
})

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 
});

Node Popups

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

tabName
string
required
Name of tab to activate: ‘sankey’, ‘network’, ‘map’, or ‘ego’
sourceEl
HTMLElement
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

tabName
string
required
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

message
string
required
Message text to display
type
'info' | 'error' | 'warning'
default:"info"
Message type for styling

Performance Considerations

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

Build docs developers (and LLMs) love