MediaWiki:Network-graph.js: Difference between revisions

From SUDEP Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
(27 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 20, 2019
Release Date: September 23, 2019


*/
*/


var Ctrl = {
var Json; // populated once when the page loads
canvas: {
var Ctrl; // rebuilt each time the graph is rendered on a given page (e.g., when resizing)
width: 800,
height: 600,
borderColor: 'black',
borderWidth: '1px'
},


color: [
function init() {
'#e6194B', // red
Ctrl = {
'#f58231', // orange
json: {},
'#ffe119', // yellow
'#3cb44b', // green
'#42d4f4', // cyan
'#4363d8', // blue
'#000075', // navy
'#f032e6', // magenta
'#800000', // maroon
'#9A6324', // brown
],


circle: {
canvas: {
radius: 10,
width: $('#network-graph').width(),
textXOffset: -4,
height: $('#network-graph').width(),
textYOffset: 4,
borderColor: 'black',
fontSize: '12px',
borderWidth: '1px'
fontColor: 'white',
},
fontWeight: 'bold',
},


arrow: {
color: [
size: 6,
'#e6194B', // red
strokeWidth: 1, // 0 = no arrow
'#f58231', // orange
color: 'grey',
'#ffe119', // yellow
},
'#3cb44b', // green
'#42d4f4', // cyan
'#4363d8', // blue
'#000075', // navy
'#f032e6', // magenta
'#800000', // maroon
'#9a6324', // brown
],


line: {
tocColor: '#48a5d1',
strokeWidth: 2,
color: 'grey',
},


physical: {
circle: {
charge: -10,
radius: 14,
linkDistance: 10,
textXOffset: -4,
distance: 120,
textYOffset: 4,
gravity: .01,
fontSize: '12px',
},
fontColor: 'white',
fontWeight: 'bold',
},


force: null,
arrow: {
svg: null,
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);
}
}


Line 69: 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 92: Line 113:


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


return baseUrl;
return baseUrl;
Line 105: 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 112: 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 133: Line 149:
// index - the index of the node that was double clicked
// index - the index of the node that was double clicked


window.location.href = getBaseUrl() + '/index.php/' + node.name;
// 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 main() {
function loadData(onSuccess) {
var title = getTitle();
$('#network-graph').html('Retrieving data for the network graph...');
var depth = getDepth();


if (depth < 1) { depth = 1; }
$.ajax({
else if (depth > 9) { depth = 9; }
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);
},
});
}


// Create the canvas
function renderGraph() {
Ctrl.svg = d3.select('#network-graph')
if (typeof Json !== 'undefined') {
.append('svg')
$('#network-graph').html('Hover over a node to see the title of the article. Double-click on a node to view the article.<br />');
.attr('width', Ctrl.canvas.width)
.attr('height', Ctrl.canvas.height);


// Define the arrow head
init();
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
// Adjust the canvas size based on the number of nodes
Ctrl.force = d3.layout
var nodes = Json.nodes.length;
.force()
if (nodes > 200) { Ctrl.circle.radius *= 2 / 3; }
.size([Ctrl.canvas.width, Ctrl.canvas.height])
var max = Ctrl.canvas.width;
.charge(Ctrl.physical.charge)
var min = Math.sqrt(nodes) * Ctrl.circle.radius * 10;
.linkDistance(Ctrl.physical.linkDistance)
if (min < max) { Ctrl.canvas.width = Ctrl.canvas.height = min; }
.distance(Ctrl.physical.distance)
.gravity(Ctrl.physical.gravity);


// Read the data and use it to build the graph
// Create the canvas
var url = getBaseUrl() + '/site-links/network-graph.php?depth=' + depth + '&title=' + title;
Ctrl.svg = d3.select('#network-graph')
d3.json(url, function(json) {
.append('svg')
if (json) {
.attr('width', Ctrl.canvas.width)
// Introduce the data to the force simulation
.attr('height', Ctrl.canvas.height);
Ctrl.force
.nodes(json.nodes)
.links(json.links);


// Add the links (lines and arrows)
// 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
Ctrl.svg
.selectAll('.link')
.append('defs')
.data(json.links)
.selectAll('marker')
.data(['arrow' + level])
.enter()
.enter()
.append('line')
.append('marker')
.attr('class', 'link')
.attr('id', function(node, index) { return node; }) // e.g., 'arrow0'
.style('stroke', Ctrl.line.color)
.attr('viewBox', '0 -' + halfSize + ' ' + fullSize + ' ' + fullSize)
.style('stroke-width', Ctrl.line.strokeWidth)
.attr('refX', function(node, index) { return Math.floor(Ctrl.circle.radius / 2) + Ctrl.arrow.size; })
.style('marker-end', function(node, index) { return 'url(#arrow0)'; }); //return 'url(#arrow' + node.level + ')'; });
.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)
// 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')
.attr('class', 'node')
.attr('class', 'node')
.on('dblclick', gotoPage)
.on('dblclick', gotoPage)
.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);
 
// 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);


// Add a title to each node
// Set up the tick handler
Ctrl.force.on('tick', function() {
Ctrl.svg
Ctrl.svg
.selectAll('circle')
.selectAll('.link')
.append('title')
.attr('x1', function(link) { return inBounds(link.source.x); })
.text(function(node, index) { return node.name.replace(/_/g, ' ').toTitleCase(); });
.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); });


// Add the level number to each node
Ctrl.svg
Ctrl.svg
.selectAll('.node')
.selectAll('.node')
.append('text')
.attr('transform', function(node) {
.attr('dx', Ctrl.circle.textXOffset)
return 'translate(' + inBounds(node.x) + ',' + inBounds(node.y) + ')';
.attr('dy', Ctrl.circle.textYOffset)
});
.text(function(node, index) { return node.level; })
});
.style('font-size', Ctrl.circle.fontSize)
.style('fill', Ctrl.circle.fontColor)
.style('font-weight', Ctrl.circle.fontWeight);


// Draw a border around the graph
// Start the force simulation
Ctrl.svg.append('rect')
Ctrl.force.start();
.attr('x', 0)
}
.attr('y', 0)
}
.attr('height', Ctrl.canvas.height)
.attr('width', Ctrl.canvas.width)
.style('stroke', Ctrl.canvas.borderColor)
.style('fill', 'none')
.style('stroke-width', Ctrl.canvas.borderWidth);


// Set up the tick handler
function main() {
Ctrl.force.on('tick', function() { tickHandler(); });
$(window).resize(function() {
renderGraph();
});


// Start the force simulation
loadData(renderGraph);
Ctrl.force.start();
$('#loadNetworkGraph').html('');
} else {
$('#loadNetworkGraph').html('No data is available for this graph.');
}
});
}
}


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

Latest revision as of 18: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://testtestsudepwiki.pathologyjhmi.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(); });