Difference between revisions of "MediaWiki:Network-graph.js"

From SUDEP Wiki
Jump to navigation Jump to search
 
(18 intermediate revisions by the same user not shown)
Line 6: Line 6:
 
depth = the number of levels to include in the graph (1 - 9)
 
depth = the number of levels to include in the graph (1 - 9)
 
Author: Alan O'Neill
 
Author: Alan O'Neill
Release Date: June 24, 2019
+
Release Date: September 23, 2019
  
 
*/
 
*/
  
var Ctrl;
+
var Json; // populated once when the page loads
 +
var Ctrl; // rebuilt each time the graph is rendered on a given page (e.g., when resizing)
  
 
function init() {
 
function init() {
 
Ctrl = {
 
Ctrl = {
json: null,
+
json: {},
  
 
canvas: {
 
canvas: {
Line 33: Line 34:
 
'#f032e6', // magenta
 
'#f032e6', // magenta
 
'#800000', // maroon
 
'#800000', // maroon
'#9A6324', // brown
+
'#9a6324', // brown
 
],
 
],
 +
 +
tocColor: '#48a5d1',
  
 
circle: {
 
circle: {
Line 60: Line 63:
 
linkDistance: 10,
 
linkDistance: 10,
 
distance: 120,
 
distance: 120,
gravity: .01,
+
gravity: .02,
 
},
 
},
  
Line 66: Line 69:
 
svg: null,
 
svg: null,
 
}
 
}
 +
 +
$.extend(Ctrl.json, Json);
 
}
 
}
  
Line 75: Line 80:
 
}
 
}
 
);
 
);
 +
}
 +
 +
function inBounds(n) {
 +
// Adjust a coordinate so it is within the bounds of the canvas (remember, it's always square)
 +
// n - the number to adjust
 +
// Return the adjusted value
 +
 +
var min = Ctrl.circle.radius * 3;
 +
var max = Ctrl.canvas.width - min;
 +
return Math.min(Math.max(n, min), max);
 
}
 
}
  
Line 113: Line 128:
 
title = location.pathname.substr(location.pathname.indexOf(delim) + delim.length); // e.g., Cerebral_hemispheric_lateralization_in_cardiac_autonomic_control
 
title = location.pathname.substr(location.pathname.indexOf(delim) + delim.length); // e.g., Cerebral_hemispheric_lateralization_in_cardiac_autonomic_control
 
}
 
}
 +
title = decodeURIComponent(title);
  
 
return title;
 
return title;
Line 120: Line 136:
 
// Return the depth to use for the graph (?)
 
// Return the depth to use for the graph (?)
  
return 9;
+
var depth = 9;
}
 
  
function tickHandler() {
+
if (depth < 1) { depth = 1; }
Ctrl.svg
+
else if (depth > 9) { depth = 9; }
.selectAll('.link')
 
.attr('x1', function(link) { return link.source.x; })
 
.attr('y1', function(link) { return link.source.y; })
 
.attr('x2', function(link) { return link.target.x; })
 
.attr('y2', function(link) { return link.target.y; });
 
  
Ctrl.svg
+
return depth;
.selectAll('.node')
 
.attr('transform', function(node) { return 'translate(' + node.x + ',' + node.y + ')'; });
 
 
}
 
}
  
Line 149: Line 157:
 
};
 
};
  
function renderGraph() {
+
function loadData(onSuccess) {
$('#network-graph').html('<div id="network-graph-caption">Retrieving data for the network graph...</div>');
+
$('#network-graph').html('Retrieving data for the network graph...');
  
init();
+
$.ajax({
 +
method: 'get',
 +
url: getBaseUrl() + '/site-links/network-graph.php',
 +
data: { depth: getDepth(), title: getTitle() },
 +
dataType: 'json',
 +
success: function(data, textStatus, jqXHR) {
 +
if (data == 0) {
 +
$('#network-graph').html('No data is available for this graph.');
 +
} else {
 +
Json = data;
 +
onSuccess();
 +
}
 +
},
 +
error: function(jqXHR, textStatus, errorThrown) {
 +
$('#network-graph').html('An error occurred while retrieving data for this graph.');
 +
console.log('loadData: jqXHR =', jqXHR, '; textStatus =', textStatus, '; errorThrown =', errorThrown);
 +
},
 +
});
 +
}
  
var title = getTitle();
+
function renderGraph() {
var depth = getDepth();
+
if (typeof Json !== 'undefined') {
 +
$('#network-graph').html('Hover over a node to see the title of the article. Double-click on a node to view the article.<br />');
  
if (depth < 1) { depth = 1; }
+
init();
else if (depth > 9) { depth = 9; }
 
  
// Create the canvas
+
// Adjust the canvas size based on the number of nodes
Ctrl.svg = d3.select('#network-graph')
+
var nodes = Json.nodes.length;
.append('svg')
+
if (nodes > 200) { Ctrl.circle.radius *= 2 / 3; }
.attr('width', Ctrl.canvas.width)
+
var max = Ctrl.canvas.width;
.attr('height', Ctrl.canvas.height);
+
var min = Math.sqrt(nodes) * Ctrl.circle.radius * 10;
 +
if (min < max) { Ctrl.canvas.width = Ctrl.canvas.height = min; }
  
// Define the arrow head
+
// Create the canvas
for (var level = 0; level < depth; level++ ) {
+
Ctrl.svg = d3.select('#network-graph')
let fullSize = Ctrl.arrow.size;
+
.append('svg')
let halfSize = Math.floor(fullSize / 2);
+
.attr('width', Ctrl.canvas.width)
Ctrl.svg
+
.attr('height', Ctrl.canvas.height);
.append('defs')
 
.selectAll('marker')
 
.data(['arrow' + level])
 
.enter()
 
.append('marker')
 
.attr('id', function(node, index) { return node; }) // e.g., 'arrow0'
 
.attr('viewBox', '0 -' + halfSize + ' ' + fullSize + ' ' + fullSize)
 
.attr('refX', function(node, index) { return Math.floor(Ctrl.circle.radius / 2) + Ctrl.arrow.size; })
 
.attr('refY', 0)
 
.attr('markerWidth', fullSize)
 
.attr('markerHeight', fullSize)
 
.attr('orient', 'auto')
 
.append('path')
 
.attr('d', function(node, index) { return 'M0,-' + halfSize + 'L' + fullSize + ',0L0,' + halfSize + 'L' + fullSize + ',0L0,-' + halfSize; })
 
.style('stroke', Ctrl.arrow.color)
 
.style('stroke-width', Ctrl.arrow.strokeWidth)
 
}
 
  
// Define the characteristics of the force simulation
+
// Define the arrow head
Ctrl.force = d3.layout
+
var depth = getDepth();
.force()
+
for (var level = 0; level < depth; level++ ) {
.size([Ctrl.canvas.width, Ctrl.canvas.height])
+
let fullSize = Ctrl.arrow.size;
.charge(Ctrl.physical.charge)
+
let halfSize = Math.floor(fullSize / 2);
.linkDistance(Ctrl.physical.linkDistance)
+
Ctrl.svg
.distance(Ctrl.physical.distance)
+
.append('defs')
.gravity(Ctrl.physical.gravity);
+
.selectAll('marker')
 
+
.data(['arrow' + level])
// Read the data and use it to build the graph
+
.enter()
//var url = getBaseUrl() + '/site-links/network-graph.php?depth=' + depth + '&title=' + title;
+
.append('marker')
//d3.json(url, function(json) {
+
.attr('id', function(node, index) { return node; }) // e.g., 'arrow0'
// if (Ctrl.json) {
+
.attr('viewBox', '0 -' + halfSize + ' ' + fullSize + ' ' + fullSize)
// }
+
.attr('refX', function(node, index) { return Math.floor(Ctrl.circle.radius / 2) + Ctrl.arrow.size; })
//});
+
.attr('refY', 0)
 
+
.attr('markerWidth', fullSize)
if (! Ctrl.json) {
+
.attr('markerHeight', fullSize)
$.ajax({
+
.attr('orient', 'auto')
method:        'get',
+
.append('path')
url:            getBaseUrl() + '/site-links/network-graph.php,
+
.attr('d', function(node, index) { return 'M0,-' + halfSize + 'L' + fullSize + ',0L0,' + halfSize + 'L' + fullSize + ',0L0,-' + halfSize; })
data:          { depth: depth, title: title },
+
.style('stroke', Ctrl.arrow.color)
dataType:      'json',
+
.style('stroke-width', Ctrl.arrow.strokeWidth)
success:        function(data, textStatus, jqXHR) {
+
}
Ctrl.json = json;
 
 
},
 
error:          function(jqXHR, textStatus, errorThrown) {
 
var msg;
 
if ('responseJSON' in jqXHR && 'error' in jqXHR.responseJSON) {
 
msg = jqXHR.responseJSON.error;
 
} else {
 
msg = textStatus + ': ' + errorThrown;
 
}
 
alert(msg);
 
},
 
});
 
}
 
  
var json;
+
// Define the characteristics of the force simulation
$.extend(json, Ctrl.json);
+
Ctrl.force = d3.layout
 +
.force()
 +
.size([Ctrl.canvas.width, Ctrl.canvas.height])
 +
.charge(Ctrl.physical.charge)
 +
.linkDistance(Ctrl.physical.linkDistance)
 +
.distance(Ctrl.physical.distance)
 +
.gravity(Ctrl.physical.gravity);
  
if (json) {
 
 
// Introduce the data to the force simulation
 
// Introduce the data to the force simulation
 
Ctrl.force
 
Ctrl.force
.nodes(json.nodes)
+
.nodes(Ctrl.json.nodes)
.links(json.links);
+
.links(Ctrl.json.links);
  
 
// Add the links (lines and arrows)
 
// Add the links (lines and arrows)
 
Ctrl.svg
 
Ctrl.svg
 
.selectAll('.link')
 
.selectAll('.link')
.data(json.links)
+
.data(Ctrl.json.links)
 
.enter()
 
.enter()
 
.append('line')
 
.append('line')
Line 245: Line 246:
 
.style('stroke', Ctrl.line.color)
 
.style('stroke', Ctrl.line.color)
 
.style('stroke-width', Ctrl.line.strokeWidth)
 
.style('stroke-width', Ctrl.line.strokeWidth)
.style('marker-end', function(node, index) { return 'url(#arrow0)'; }); //return 'url(#arrow' + node.level + ')'; });
+
.style('marker-end', function(node, index) { return 'url(#arrow0)'; });
  
 
// Add the nodes (circles)
 
// Add the nodes (circles)
 
Ctrl.svg
 
Ctrl.svg
 
.selectAll('.node')
 
.selectAll('.node')
.data(json.nodes)
+
.data(Ctrl.json.nodes)
 
.enter()
 
.enter()
 
.append('g')
 
.append('g')
Line 257: Line 258:
 
.append('circle')
 
.append('circle')
 
.attr('r', Ctrl.circle.radius)
 
.attr('r', Ctrl.circle.radius)
.style('fill', function(node, index) { return Ctrl.color[node.level]; })
+
.style('fill', function(node, index) { return (node.level < 100) ? Ctrl.color[node.level] : Ctrl.tocColor; })
 
.call(Ctrl.force.drag);
 
.call(Ctrl.force.drag);
  
Line 276: Line 277:
 
.attr('dx', Ctrl.circle.textXOffset)
 
.attr('dx', Ctrl.circle.textXOffset)
 
.attr('dy', Ctrl.circle.textYOffset)
 
.attr('dy', Ctrl.circle.textYOffset)
.text(function(node, index) { return node.level; })
+
.text(function(node, index) { return (node.level < 100) ? node.level : ''; })
 
.style('font-size', Ctrl.circle.fontSize)
 
.style('font-size', Ctrl.circle.fontSize)
 
.style('fill', Ctrl.circle.fontColor)
 
.style('fill', Ctrl.circle.fontColor)
.style('font-weight', Ctrl.circle.fontWeight);
+
.style('font-weight', Ctrl.circle.fontWeight)
 
+
.append('title')
 +
.text(function(node, index) {
 +
var title = node.name.replace(/_/g, ' ').toTitleCase();
 +
if (node.level == 0) { title += ' (this page)'; }
 +
return title;
 +
});
 +
 
// Draw a border around the graph
 
// Draw a border around the graph
 
Ctrl.svg.append('rect')
 
Ctrl.svg.append('rect')
 
.attr('x', 0)
 
.attr('x', 0)
 
.attr('y', 0)
 
.attr('y', 0)
.attr('height', Ctrl.canvas.height)
+
.attr('height', Ctrl.canvas.height - 1)
.attr('width', Ctrl.canvas.width)
+
.attr('width', Ctrl.canvas.width - 1)
 
.style('stroke', Ctrl.canvas.borderColor)
 
.style('stroke', Ctrl.canvas.borderColor)
 
.style('fill', 'none')
 
.style('fill', 'none')
Line 292: Line 299:
  
 
// Set up the tick handler
 
// Set up the tick handler
Ctrl.force.on('tick', function() { tickHandler(); });
+
Ctrl.force.on('tick', function() {
 +
Ctrl.svg
 +
.selectAll('.link')
 +
.attr('x1', function(link) { return inBounds(link.source.x); })
 +
.attr('y1', function(link) { return inBounds(link.source.y); })
 +
.attr('x2', function(link) { return inBounds(link.target.x); })
 +
.attr('y2', function(link) { return inBounds(link.target.y); });
 +
 
 +
Ctrl.svg
 +
.selectAll('.node')
 +
.attr('transform', function(node) {
 +
return 'translate(' + inBounds(node.x) + ',' + inBounds(node.y) + ')';
 +
});
 +
});
  
 
// Start the force simulation
 
// Start the force simulation
Ctrl.force.start();
+
Ctrl.force.start();
$('#network-graph-caption').html('Hover over a node to see the title of the article. Double-click on a node to view the article.');
 
} else {
 
$('#network-graph-caption').html('No data is available for this graph.');
 
 
}
 
}
 
}
 
}
Line 307: Line 324:
 
});
 
});
  
renderGraph();
+
loadData(renderGraph);
 
}
 
}
  
 
$(document).ready(function() { main(); });
 
$(document).ready(function() { main(); });

Latest revision as of 14:06, 23 September 2019

/*

Name:		MediaWiki:Network-graph.js
Purpose:	Build the network graph for the page being displayed
Parameters:	title = the internal title of the reference (e.g., Cerebral_hemispheric_lateralization_in_cardiac_autonomic_control)
		depth = the number of levels to include in the graph (1 - 9)
Author:		Alan O'Neill
Release Date:	September 23, 2019

*/

var Json; // populated once when the page loads
var Ctrl; // rebuilt each time the graph is rendered on a given page (e.g., when resizing)

function init() {
	Ctrl = {
		json:	{},

		canvas: {
			width:		$('#network-graph').width(),
			height:		$('#network-graph').width(),
			borderColor:	'black',
			borderWidth:	'1px'
		},

		color: [
			'#e6194B',	// red
			'#f58231',	// orange
			'#ffe119',	// yellow
			'#3cb44b',	// green
			'#42d4f4',	// cyan
			'#4363d8',	// blue
			'#000075',	// navy
			'#f032e6',	// magenta
			'#800000',	// maroon
			'#9a6324',	// brown
		],

		tocColor:		'#48a5d1',

		circle: {
			radius:		14,
			textXOffset:	-4,
			textYOffset:	4,
			fontSize:	'12px',
			fontColor:	'white',
			fontWeight:	'bold',
		},

		arrow: {
			size:		6,
			strokeWidth:	1,		// 0 = no arrow
			color:		'grey',
		},

		line: {
			strokeWidth:	2,
			color:		'grey',
		},

		physical: {
			charge:		-10,
			linkDistance:	10,
			distance:	120,
			gravity:	.02,
		},

		force:			null,
		svg:			null,
	}

	$.extend(Ctrl.json, Json);
}

String.prototype.toTitleCase = function() {
	return this.replace(
		/\w\S*/g,
		function(txt) {
			return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
		}
	);
}

function inBounds(n) {
	// Adjust a coordinate so it is within the bounds of the canvas (remember, it's always square)
	// n - the number to adjust
	// Return the adjusted value
	
	var min = Ctrl.circle.radius * 3;
	var max = Ctrl.canvas.width - min;
	return Math.min(Math.max(n, min), max);
}

function getQuery(name) {
	// Return the value of a parameter in the query string
	// name - the name in the name/value pair

	var value = '';
	var query = window.location.search.substring(1).split('&');
	for (var i = 0; i < query.length; i++) {
		var nv = query[i].split('=');
		if (nv[0] == name) {
			value = nv[1];
			break;
		}
	}

	return value;
}

function getBaseUrl() {
	// Return the base URL for the site (e.g., 'https://sudepwiki.pathology.jhmi.edu')

	var baseUrl = location.protocol + '//' + location.host + location.pathname;
	var i = baseUrl.indexOf('/index.php');
	if (i < 0) { i = baseUrl.indexOf('/site-links'); }
	baseUrl = baseUrl.substr(0, i);

	return baseUrl;
}

function getTitle() {
	// Return the title of the article from the URL

	var title = getQuery('title');
	if (title.length == 0) {
		var delim = '/index.php/';
		title = location.pathname.substr(location.pathname.indexOf(delim) + delim.length); // e.g., Cerebral_hemispheric_lateralization_in_cardiac_autonomic_control
	}
	title = decodeURIComponent(title);

	return title;
}

function getDepth() {
	// Return the depth to use for the graph (?)

	var depth = 9;

	if (depth < 1) { depth = 1; }
	else if (depth > 9) { depth = 9; }

	return depth;
}

function gotoPage(node, index) {
	// Go to the page associated with a node
	// node - the node that was double clicked (object)
	// index - the index of the node that was double clicked

	// When node.level is zero, the node refers to the page being displayed
	if (node.level > 0) {
		window.location.href = getBaseUrl() + '/index.php/' + node.name;
	} else {
		alert("You're already viewing this page.");
	}
};

function loadData(onSuccess) {
	$('#network-graph').html('Retrieving data for the network graph...');

	$.ajax({
		method:		'get',
		url:		getBaseUrl() + '/site-links/network-graph.php',
		data:		{ depth: getDepth(), title: getTitle() },
		dataType:	'json',
		success:	function(data, textStatus, jqXHR) {
					if (data == 0) {
						$('#network-graph').html('No data is available for this graph.');
					} else {
						Json = data;
						onSuccess();
					}
				},
		error:		function(jqXHR, textStatus, errorThrown) {
					$('#network-graph').html('An error occurred while retrieving data for this graph.');
					console.log('loadData: jqXHR =', jqXHR, '; textStatus =', textStatus, '; errorThrown =', errorThrown);
				},
	});
}

function renderGraph() {
	if (typeof Json !== 'undefined') {
		$('#network-graph').html('Hover over a node to see the title of the article. Double-click on a node to view the article.<br />');

		init();

		// Adjust the canvas size based on the number of nodes
		var nodes = Json.nodes.length;
		if (nodes > 200) { Ctrl.circle.radius *= 2 / 3; }
		var max = Ctrl.canvas.width;
		var min = Math.sqrt(nodes) * Ctrl.circle.radius * 10;
		if (min < max) { Ctrl.canvas.width = Ctrl.canvas.height = min; }

		// Create the canvas
		Ctrl.svg = d3.select('#network-graph')
			.append('svg')
			.attr('width', Ctrl.canvas.width)
			.attr('height', Ctrl.canvas.height);

		// Define the arrow head
		var depth = getDepth();
		for (var level = 0; level < depth; level++ ) {
			let fullSize = Ctrl.arrow.size;
			let halfSize = Math.floor(fullSize / 2);
			Ctrl.svg
				.append('defs')
				.selectAll('marker')
				.data(['arrow' + level])
				.enter()
				.append('marker')
				.attr('id', function(node, index) { return node; }) // e.g., 'arrow0'
				.attr('viewBox', '0 -' + halfSize + ' ' + fullSize + ' ' + fullSize)
				.attr('refX', function(node, index) { return Math.floor(Ctrl.circle.radius / 2) + Ctrl.arrow.size; })
				.attr('refY', 0)
				.attr('markerWidth', fullSize)
				.attr('markerHeight', fullSize)
				.attr('orient', 'auto')
				.append('path')
				.attr('d', function(node, index) { return 'M0,-' + halfSize + 'L' + fullSize + ',0L0,' + halfSize + 'L' + fullSize + ',0L0,-' + halfSize; })
				.style('stroke', Ctrl.arrow.color)
				.style('stroke-width', Ctrl.arrow.strokeWidth)
		}

		// Define the characteristics of the force simulation
		Ctrl.force = d3.layout
			.force()
			.size([Ctrl.canvas.width, Ctrl.canvas.height])
			.charge(Ctrl.physical.charge)
			.linkDistance(Ctrl.physical.linkDistance)
			.distance(Ctrl.physical.distance)
			.gravity(Ctrl.physical.gravity);

		// Introduce the data to the force simulation
		Ctrl.force
			.nodes(Ctrl.json.nodes)
			.links(Ctrl.json.links);

		// Add the links (lines and arrows)
		Ctrl.svg
			.selectAll('.link')
			.data(Ctrl.json.links)
			.enter()
			.append('line')
			.attr('class', 'link')
			.style('stroke', Ctrl.line.color)
			.style('stroke-width', Ctrl.line.strokeWidth)
			.style('marker-end', function(node, index) { return 'url(#arrow0)'; });

		// Add the nodes (circles)
		Ctrl.svg
			.selectAll('.node')
			.data(Ctrl.json.nodes)
			.enter()
			.append('g')
			.attr('class', 'node')
			.on('dblclick', gotoPage)
			.append('circle')
			.attr('r', Ctrl.circle.radius)
			.style('fill', function(node, index) { return (node.level < 100) ? Ctrl.color[node.level] : Ctrl.tocColor; })
			.call(Ctrl.force.drag);

		// Add a title to each node
		Ctrl.svg
			.selectAll('circle')
			.append('title')
			.text(function(node, index) {
				var title = node.name.replace(/_/g, ' ').toTitleCase();
				if (node.level == 0) { title += ' (this page)'; }
				return title;
			});

		// Add the level number to each node
		Ctrl.svg
			.selectAll('.node')
			.append('text')
			.attr('dx', Ctrl.circle.textXOffset)
			.attr('dy', Ctrl.circle.textYOffset)
			.text(function(node, index) { return (node.level < 100) ? node.level : ''; })
			.style('font-size', Ctrl.circle.fontSize)
			.style('fill', Ctrl.circle.fontColor)
			.style('font-weight', Ctrl.circle.fontWeight)
			.append('title')
			.text(function(node, index) {
				var title = node.name.replace(/_/g, ' ').toTitleCase();
				if (node.level == 0) { title += ' (this page)'; }
				return title;
			});
			
		// Draw a border around the graph
		Ctrl.svg.append('rect')
			.attr('x', 0)
			.attr('y', 0)
			.attr('height', Ctrl.canvas.height - 1)
			.attr('width', Ctrl.canvas.width - 1)
			.style('stroke', Ctrl.canvas.borderColor)
			.style('fill', 'none')
			.style('stroke-width', Ctrl.canvas.borderWidth);

		// Set up the tick handler
		Ctrl.force.on('tick', function() {
			Ctrl.svg
				.selectAll('.link')
				.attr('x1', function(link) { return inBounds(link.source.x); })
				.attr('y1', function(link) { return inBounds(link.source.y); })
				.attr('x2', function(link) { return inBounds(link.target.x); })
				.attr('y2', function(link) { return inBounds(link.target.y); });

			Ctrl.svg
				.selectAll('.node')
				.attr('transform', function(node) {
					return 'translate(' + inBounds(node.x) + ',' + inBounds(node.y) + ')';
				});
		});

		// Start the force simulation
		Ctrl.force.start();	
	}
}

function main() {
	$(window).resize(function() {
		renderGraph();
	});

	loadData(renderGraph);
}

$(document).ready(function() { main(); });