website/static/network.js
function prepareSVG(element)
{
return d3
.select(element)
.append('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('class', 'svg-content-responsive')
}
function clone(object)
{
// this implementation won't handle functions and
// more advanced objects - only simple key-values
return JSON.parse(JSON.stringify(object))
}
var Network = function ()
{
// data variables
var sites
var groups
var central_node
var force_manager
// visualisation variables
var nodes
var svg
var links
var zoom
var edges = []
var orbits
var dispatch = d3.dispatch('networkmove')
var types = {
kinase: new String('Kinase'),
drug: new String('Drug'),
group: new String('Family or group'),
site: new String('Site'),
central: new String('Analysed protein')
}
var tooltip
function fitTextIntoCircle(d, context)
{
var radius = d.r
return Math.min(2 * radius, (2 * radius - 8) / context.getComputedTextLength() * 24)
}
function calculateRadius()
{
return config.radius
}
function createProteinNode(protein)
{
var radius = calculateRadius()
var name = protein.name
if(!protein.is_preferred)
{
name += '\n(' + protein.refseq + ')'
}
var pos = zoom.viewport_to_canvas([(config.width - radius) / 2, (config.height - radius) / 2]);
return {
type: types.central,
name: name,
r: radius,
x: pos[0],
y: pos[1],
fixed: true,
protein: protein
}
}
var config = {
// Required
element: null, // specifies where network will be embedded
data: null, // json-serialized network definition
// Dimensions
width: 600,
height: null,
ratio: 1, // the aspect ratio
responsive: true, /* if responsive is set, (width, height and ratio) does not count:
the dimensions will be adjusted to fit `element` boundaries */
// Configuration
show_sites: true,
clone_by_site: true,
default_link_distance: 100,
site_kinase_link_weight: 1.15,
collide_drugs: true,
// Element sizes
site_size_unit: 5,
radius: 6, // of a single node
// Callbacks
nodeURL: (function(node) {
return window.location.href + '#' + node.protein.refseq
}),
// Zoom
min_zoom: 1/4, // allow to zoom-out up to four times
max_zoom: 3 // allow to zoom-in up to three times
}
function configure(new_config, callback)
{
// Automatic configuration update:
update_object(config, new_config);
get_remote_if_needed(config, 'data', callback)
}
function select_kinases_by_name(names, kinases)
{
var all_matching_kinases = [];
for(var i = 0; i < names.length; i++)
{
var name = names[i];
var matching_kinases = kinases.filter(function(kinase) {return kinase.name === name});
append(all_matching_kinases, matching_kinases);
}
return all_matching_kinases
}
function addEdge(source, target, weight)
{
weight = weight || 1
edges.push(
{
source: source,
target: target,
weight: weight
}
)
}
function prepareSites(sites, standalone_kinases)
{
var cloned_kinases = []
function parse(site)
{
site.visible_interactors = 0
site.name = site.position + ' ' + site.residue
site.size = Math.max(site.name.length, 6) * config.site_size_unit
// the site visualised as a square has bounding radius of outscribed circle on that square
site.size += site.mutations_count / 2
site.r = Math.sqrt(site.size * site.size / 4)
site.type = types.site
site.collisions_active = true
// this property will be populated for kinases belonging to group in prepareKinaseGroups
site.group = undefined
// make links to the central protein's node from this site
addEdge(site, central_node)
var site_kinases = select_kinases_by_name(site.kinases, standalone_kinases);
append(site_kinases, select_kinases_by_name(site.kinase_groups, groups));
site.interactors = [];
function associate_kinase_with_site(kinase_template)
{
var kinase;
if(config.clone_by_site)
{
if(kinase_template.used)
{
kinase = clone(kinase_template);
cloned_kinases.push(kinase)
}
else {
kinase_template.used = true
kinase = kinase_template
}
}
else {
kinase = kinase_template
}
addEdge(kinase, site, config.site_kinase_link_weight);
site.interactors.push(kinase);
site.visible_interactors += 1
}
site_kinases.forEach(associate_kinase_with_site);
}
sites.forEach(parse);
return cloned_kinases
}
function prepareKinases(all_kinases)
{
// If kinase occurs both in a group and bounds to
// the central protein, duplicate it's node. How?
// 1. duplicate the data
// 2. make the notion in the data and just add two circles
// And currently it is implemented by data duplication
var kinases = []
for(var i = 0; i < all_kinases.length; i++)
{
var kinase = all_kinases[i]
kinase.type = types.kinase
kinase.collisions_active = false
kinase.r = calculateRadius()
// this property will be populated for kinases belonging to group in prepareKinaseGroups
kinase.group = undefined
// will be populated if and when sites created
kinase.site = undefined
if(central_node.protein.kinases.indexOf(kinase.name) !== -1)
{
// add a kinase that binds to the central protein to `kinases` list
kinase = clone(kinase)
kinases.push(kinase)
if(!config.show_sites)
{
// make links to the central protein's node from those
// kinases that bound to the central protein (i.e.
// exclude those which are shown only in groups)
addEdge(kinase, central_node)
}
}
}
return kinases
}
function prepareKinaseGroups(groups, all_kinases)
{
var cloned_kinases = [];
function parse(group)
{
group.kinases_names = group.kinases;
group.type = types.group;
group.drugs = []; // No drugs for kinase groups are displayed currently
function is_in_group(kinase)
{
return group.kinases_names.indexOf(kinase.name) !== -1
}
var group_kinases = all_kinases.filter(is_in_group);
var mutations_in_kinases = 0;
function associate_kinase_with_group(kinase_template)
{
var kinase = clone(kinase_template);
cloned_kinases.push(kinase);
kinase.group = group;
mutations_in_kinases += kinase.protein ? kinase.protein.mutations_count : 0;
addEdge(kinase, group);
}
group_kinases.forEach(associate_kinase_with_group);
group.r = calculateRadius();
if(!config.show_sites)
addEdge(group, central_node)
}
groups.forEach(parse);
return cloned_kinases;
}
function linkDistance(edge)
{
// if a node wants to overwrite other behaviours, let him
if(edge.source.link_distance)
{
return edge.source.link_distance
}
// let's place sites (or kinases when show sites = false) in layers around the central protein
if(edge.target.index === 0)
{
return orbits.getRadiusByNode(edge.source)
}
// dynamically adjust the length of a link between
// a kinase located in a group and its group's node
if(edge.target.type === types.group) // target node is a group
{
if(edge.target.expanded)
{
return edge.target.r + edge.source.r
}
return 0
}
return config.default_link_distance / edge.weight
}
function ForceManager(config)
{
var force_affected_nodes = [];
var force_affected_links = [];
var force;
force = d3.layout.force()
.friction(0.5)
.gravity(0)
.distance(100)
.charge(charge)
.size(config.size)
.linkDistance(linkDistance);
var public_space = {
drag: force.drag,
charge: force.charge,
start: force.start,
stop: force.stop,
on: force.on,
set_size: function(size) {
force.size(size)
},
hide_nodes_links: function(node)
{
var to_remove = [];
for(var e = 0; e < force_affected_links.length; e++)
{
var edge = force_affected_links[e];
// noinspection EqualityComparisonWithCoercionJS
if(edge.source == node)
to_remove.push(edge)
}
for(var r = 0; r < to_remove.length; r++)
{
var index = force_affected_links.indexOf(to_remove[r]);
force_affected_links.splice(index, 1);
}
node._removed_links = to_remove;
},
restore_nodes_links: function(node)
{
if(node._removed_links)
{
for(var e = 0; e < node._removed_links.length; e++)
{
force_affected_links.push(node._removed_links[e])
}
}
},
hide_from_force: function(node)
{
var index = force_affected_nodes.indexOf(node);
if(index !== -1)
force_affected_nodes.splice(index, 1)
},
make_visible_for_force: function(node)
{
force_affected_nodes.push(node)
},
settle_force: function(n)
{
force.start();
for (var i = n; i > 0; --i) force.tick()
force.stop();
},
update_force_affected_nodes: function(nodes_data, links_data)
{
if(nodes_data)
append(force_affected_nodes, nodes_data);
if(links_data)
append(force_affected_links, links_data);
force.nodes(force_affected_nodes);
force.links(force_affected_links);
}
}
return public_space;
}
function create_versor(x, y)
{
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
if(length === 0)
return [0, 0]
else
return [x / length, y /length]
}
function switchGroupState(group, state, time)
{
time = (time === undefined) ? 600 : time
group.expanded = (state === undefined) ? !group.expanded : state
function inGroup(d)
{
return group === d.group
}
function fadeInOut(selection)
{
selection
.transition().ease('linear').duration(time)
.attr('opacity', group.expanded ? 1 : 0)
}
refresh_group_collisions_state(group);
nodes
.filter(inGroup)
.each(function(node){
node.collapsed = !group.expanded;
group.visible_interactors += (node.collapsed ? -1 : +1)
if(node.collapsed)
{
node.x = group.x;
node.y = group.y;
force_manager.hide_from_force(node);
force_manager.hide_nodes_links(node);
}
else
{
var center_of_interest;
if(group.site && group.site.exposed)
center_of_interest = group.site;
else
center_of_interest = central_node;
var versor = create_versor(center_of_interest.x - group.x, center_of_interest.y - group.y);
versor = versor.map(function (c) {
return Math.random() * 2 * node.r * c
});
node.x = group.x - versor[0];
node.y = group.y - versor[1];
force_manager.restore_nodes_links(node);
force_manager.make_visible_for_force(node);
}
})
var scale = group.expanded ? 1 : 0
nodes.selectAll('.shape')
.filter(inGroup)
.transition().ease('linear').duration(time)
.attr('transform', 'scale(' + scale + ')')
nodes.selectAll('.name')
.filter(inGroup)
.call(fadeInOut)
links
.filter(function(e) { return inGroup(e.source) } )
.call(fadeInOut)
force_manager.update_force_affected_nodes()
}
function focusOn(node, radius, animation_speed)
{
var area = radius * 2.2
zoom.center_on([node.x, node.y], area, animation_speed)
}
function charge(node)
{
// we could disable charge for collapsed nodes completely and instead
// stick these nodes to theirs groups, but this might be inefficient
// noinspection EqualityComparisonWithCoercionJS
if(node.group){
if(node.collapsed)
{
return 0
}
if(!node.site)
return -10
return -150 / node.group.site.visible_interactors
}
// noinspection EqualityComparisonWithCoercionJS
// noinspection EqualityComparisonWithCoercionJS
if((node.type == types.kinase || node.type == types.drug || node.type == types.group) && !(node.site && node.site.exposed)){
if(!node.site)
return -10
return -150 / node.site.visible_interactors
}
return 0
}
function refresh_group_collisions_state(group) {
if(!config.show_sites) return
var site = group.site
for (var j = 0; j < group.kinases.length; j++) {
var kinase = group.kinases[j]
kinase.collisions_active = group.expanded && site.exposed
}
}
function is_site_protein_edge(site)
{
return function(edge){ return edge.source === site }
}
function stop_exposing_site(site)
{
// move node back to the orbit
site.exposed = false
site.fixed = false
site.link_distance = site.previous_link_distance
force_manager.charge(charge)
force_manager.start()
var link = links.filter(is_site_protein_edge(site)).transition().duration(3000)
link.attr('class', 'link')
tooltip.unstick()
tooltip.hide()
tooltip.ignore_next_signal()
}
function expose_site(site, camera_speed)
{
var link = links.filter(is_site_protein_edge(site)).transition().duration(3000)
// let's expose the node
site.exposed = true
var shift = get_max_radius() * 1.5
var max_distance_squared = 0
for(var i = 0; i < site.interactors.length; i++)
{
var interactor = site.interactors[i]
var distance_squared = Math.pow(site.x - interactor.x, 2) + Math.pow(site.y - interactor.y, 2)
if(distance_squared > max_distance_squared)
max_distance_squared = distance_squared
}
var versor = create_versor(site.x - central_node.x, site.y - central_node.y)
// usually there is more space on sides (x axis) than below and on top as the network is displayed
// in widescreen frame
var screen_ratio = 1.2
shift = shift * Math.abs(versor[0]) * screen_ratio + shift * Math.abs(versor[1]) / screen_ratio
var dest = [central_node.x + shift * versor[0], central_node.y + shift * versor[1]]
site.previous_link_distance = site.link_distance
site.link_distance = shift
site.x = dest[0]
site.y = dest[1]
// tooltips are annoying when popping up during camera movement
// (those are showing up just because mouse cursor hovers over many nodes when camera is moving)
tooltip.active(false);
focusOn(
{x: dest[0], y: dest[1]},
Math.sqrt(max_distance_squared) * 1.15,
camera_speed
);
setTimeout(function(){
tooltip.active(true);
site.fixed = true
}, camera_speed);
force_manager.start()
tooltip.unstick()
tooltip.hide()
tooltip.ignore_next_signal()
link.attr('class', 'link link-dimmed')
}
function node_dbl_click(node)
{
if(d3.event.defaultPrevented === false)
{
if(node.type === types.group)
{
switchGroupState(node)
tooltip.unstick()
tooltip.hide()
force_manager.start()
}
else if(node.type === types.site)
{
node.fixed = false
var site = node
var camera_speed = 2500
if(site.exposed) // site will be de-exposed
{
var return_camera_speed = camera_speed / 5;
stop_exposing_site(site);
tooltip.active(false);
public_space.zoom_fit(return_camera_speed);
setTimeout(function(){tooltip.active(true)}, return_camera_speed);
}
else // site will be exposed
{
for(var s = 0; s < sites.length; s++)
{
var tested_site = sites[s]
if(tested_site.exposed)
{
stop_exposing_site(tested_site)
}
}
expose_site(site, camera_speed)
force_manager.charge(0)
}
for(var i = 0; i < site.interactors.length; i++)
{
var interactor = site.interactors[i]
interactor.fixed = false
interactor.collisions_active = site.exposed
// noinspection EqualityComparisonWithCoercionJS
if(interactor.type == types.group)
{
refresh_group_collisions_state(interactor);
}
for(var d = 0; d < interactor.drugs.length; d++)
{
interactor.drugs[d].fixed = false
}
}
}
force_manager.on('tick', create_ticker())
}
}
function node_click(node)
{
if(d3.event.shiftKey && node !== central_node)
{
node.fixed = false
tooltip.ignore_next_signal()
}
}
function nodeHover(node, hover_in)
{
if(node.type === types.site)
{
nodes
.filter(function(d){ return node.interactors.indexOf(d) !== -1 })
.classed('hover', hover_in)
}
else
{
nodes
.filter(function(d){ return d.name === node.name })
.classed('hover', hover_in)
}
}
function collide(node_1, node_2, min_dist, min_dist_pow)
{
var x = node_1.x - node_2.x
var y = node_1.y - node_2.y
var distance = Math.pow(x, 2) + Math.pow(y, 2)
if(distance < min_dist_pow)
{
var l = Math.sqrt(distance)
var change = (min_dist - l) / 2
// rescale: to versor and then to displacement
if(l)
{
x /= l
x *= change
y /= l
y *= change
}
else {
x = min_dist / 2
y = min_dist / 2
}
node_1.x += x
node_1.y += y
node_2.x -= x
node_2.y -= y
}
}
function arrange_in_line(node_1, node_2, node_3, drugs_count_minus_one)
{
var x = node_1.x - node_2.x
var y = node_1.y - node_2.y
var angle = Math.atan2(y, x)
x = node_2.x - node_3.x
y = node_2.y - node_3.y
var drug_angle = Math.atan2(y, x)
if(Math.abs(angle - drug_angle) < 0.4 * drugs_count_minus_one)
return
var distance = Math.pow(x, 2) + Math.pow(y, 2)
var l = Math.sqrt(distance)
var dx = Math.cos(angle)
var dy = Math.sin(angle)
node_3.x = (2 * node_3.x + node_2.x - dx * l) / 3
node_3.y = (2 * node_3.y + node_2.y - dy * l) / 3
}
function collide_nodes(nodes, padding, constant_radius)
{
var collide_node
nodes = nodes.data()
if(constant_radius)
{
var min_dist = 2 * constant_radius + padding
var min_dist_squared = Math.pow(min_dist, 2)
collide_node = function(node)
{
for(var i = 0; i < nodes.length; i++)
{
var other_node = nodes[i]
if(node !== other_node)
{
collide(node, other_node, min_dist, min_dist_squared)
}
}
}
}
else
{
collide_node = function(node)
{
for(var i = 0; i < nodes.length; i++)
{
var other_node = nodes[i]
if(node !== other_node)
{
var min_dist = node.r + other_node.r + padding
collide(node, other_node, min_dist, min_dist * min_dist)
}
}
}
}
return collide_node
}
function collide_nodes_belonging_to_exposed_sites(exposed_sites, padding)
{
// all nodes have the same radius
var r = config.radius
var d = 2 * r + padding
var d2 = Math.pow(d, 2)
var kinases = []
var groups = []
exposed_sites.each(function(site){
var interactors = site.interactors;
append(kinases, interactors);
append(groups, interactors.filter(
function(node){ // noinspection EqualityComparisonWithCoercionJS
return node.type == types.group && node.expanded }
))
})
for(var g = 0; g < groups.length; g++)
{
var group = groups[g];
append(kinases, group.kinases);
}
kinases = kinases.filter(function(kinase){ return kinase.collisions_active })
function collide_sites_nodes()
{
// possibly: filter out kinases from other sites (but really, we have only one site exposed at time)
for(var i = 0; i < kinases.length; i++)
{
var kinase_one = kinases[i]
for(var j = i + 1; j < kinases.length; j++)
{
var kinase_two = kinases[j]
collide(kinase_one, kinase_two, d, d2)
}
}
}
return collide_sites_nodes
}
function create_ticker()
{
var site_nodes = nodes.filter(is_of_type(types.site));
var drug_nodes = nodes.filter(is_of_type(types.drug));
// could be done with bounding boxes instead as sites are represented as boxes
var site_collider = collide_nodes(site_nodes, 3)
var drugs_collider = collide_nodes(drug_nodes, 1, config.radius * 0.8)
var exposed_sites = site_nodes.filter(function (node) { return node.exposed });
var nodes_collider = collide_nodes_belonging_to_exposed_sites(exposed_sites, 3);
var drugs_by_site = {}
exposed_sites.each(
function(site)
{
drugs_by_site[site] = []
site.interactors.forEach(
function(kinase)
{
append(drugs_by_site[site], kinase.drugs)
}
)
}
)
function force_tick(e)
{
// keep drugs in line with kinases
exposed_sites.each(
function(site)
{
site.interactors.forEach(
function(kinase)
{
var l = kinase.drugs.length - 1
kinase.drugs.forEach(
function(drug)
{
arrange_in_line(site, kinase, drug, l)
}
)
}
)
}
)
site_nodes.each(site_collider);
exposed_sites.each(nodes_collider);
if(config.collide_drugs)
drug_nodes.each(drugs_collider);
links
.attr('x1', function(d) { return d.source.x })
.attr('y1', function(d) { return d.source.y })
.attr('x2', function(d) { return d.target.x })
.attr('y2', function(d) { return d.target.y })
nodes.attr('transform', function(d){ return 'translate(' + [d.x, d.y] + ')'} );
force_manager.start()
dispatch.networkmove(this)
}
return force_tick
}
function resize()
{
if(config.responsive)
{
var dimensions = $(svg.node()).parent().get(0).getBoundingClientRect()
config.width = dimensions.width
config.height = dimensions.height
config.ratio = dimensions.height / dimensions.width
}
else {
config.height = config.height || config.width * config.ratio
}
zoom.set_viewport_size(config.width, config.height)
}
function is_of_type(type, negation)
{
if(negation)
return function(node)
{
// noinspection EqualityComparisonWithCoercionJS
return node.type != type
}
return function(node)
{
// noinspection EqualityComparisonWithCoercionJS
return node.type == type
}
}
function create_drug_nodes(kinase_nodes)
{
var drugs = []
kinase_nodes.forEach(function(kinase) {
var kinase_drugs = []
kinase.drugs_targeting_kinase_gene.forEach(
function(target_data)
{
var drug_node = {
name: target_data['drug'].name,
r: config.radius,
type: types.drug,
group: kinase.group,
data: target_data
}
addEdge(drug_node, kinase)
kinase_drugs.push(drug_node)
}
)
kinase.drugs = kinase_drugs
append(drugs, kinase_drugs)
})
return drugs
}
function create_nodes(data)
{
groups = data.kinase_groups;
central_node = createProteinNode(data.protein);
var nodes_data = [central_node];
// kinases which are known to interact with protein are returned (standalone)
// those as well as other (i.e. kinases which are in families known to interact with protein)
// are prepared to be displayed
var standalone_kinases = prepareKinases(data.kinases);
append(nodes_data, standalone_kinases);
if(config.show_sites)
{
var cloned_kinases = prepareSites(data.sites, standalone_kinases);
sites = data.sites;
append(nodes_data, cloned_kinases.filter(is_of_type(types.kinase)));
append(nodes_data, sites);
// those which are not kinases are groups (type was not assigned yet)
append(groups, cloned_kinases.filter(is_of_type(types.kinase, true)))
}
else
{
sites = []
}
var kinases_which_are_in_groups = prepareKinaseGroups(groups, data.kinases);
append(nodes_data, groups);
append(nodes_data, kinases_which_are_in_groups);
var kinase_nodes = nodes_data.filter(is_of_type(types.kinase));
groups.forEach(function(group) {
group.kinases = []
kinase_nodes.forEach(function(node){
// noinspection EqualityComparisonWithCoercionJS
if(node.group && node.group == group)
{
group.kinases.push(node)
}
})
})
var drug_nodes = create_drug_nodes(kinase_nodes)
append(nodes_data, drug_nodes);
// not in "associate_kinase_with_site" to void circular dependencies while cloning
sites.forEach(function(site) {
site.interactors.forEach(function(kinase){
kinase.site = site
});
});
nodes_data.forEach(function(node, index) { node.id = index })
return {
all: nodes_data,
groups: groups,
sites: sites,
standalone: standalone_kinases,
kinases: kinase_nodes
}
}
function place_interactors(node, interactors, angles_per_actor, link_distance) {
var sx = central_node.x - node.x
var sy = central_node.y - node.y
var alpha = Math.atan2(sy, sx)
if (interactors.length > 1)
angles_per_actor /= interactors.length - 1
// set starting angle for first interactor
var angle = alpha - (interactors.length - 1) / 2 * angles_per_actor
for (var k = 0; k < interactors.length; k++) {
var x = Math.cos(angle) * link_distance
var y = Math.sin(angle) * link_distance
var interactor = interactors[k]
interactor.x = node.x - x
interactor.y = node.y - y
angle += angles_per_actor
}
}
function placeNodes(sites, standalone_kinases, kinases)
{
var orbiting_nodes
orbits = Orbits()
if(config.show_sites)
orbiting_nodes = sites
else
orbiting_nodes = standalone_kinases.concat(groups)
orbits.init(orbiting_nodes, central_node, {
spacing: 95,
order_by: config.show_sites ? 'kinases_count' : 'r'
})
orbits.placeNodes()
if(config.show_sites)
{
var link_distance = config.default_link_distance / config.site_kinase_link_weight
for(var i = 0; i < sites.length; i++)
{
var site = sites[i]
var site_orbit = orbits.getOrbit(site)
var angles_available_for_site = Math.PI * 2 / site_orbit.nodes_count
place_interactors(site, site.interactors, angles_available_for_site, link_distance)
}
}
var link = config.default_link_distance / config.site_kinase_link_weight
for(var j = 0; j < kinases.length; j++)
{
var kinase = kinases[j]
place_interactors(kinase, kinase.drugs, Math.PI * 2 / 360, link)
}
}
function create_color_scale(domain, range)
{
return d3.scale
.linear()
.domain(domain)
.interpolate(d3.interpolateRgb)
.range(range)
}
function get_max_radius()
{
var radius = orbits.getMaximalRadius()
if(config.show_sites)
radius += config.default_link_distance
return radius
}
function create_shapes()
{
function radians(x){
return x * Math.PI / 180
}
var ich_outer_angle = radians(150) // between 120 and 180
var ich_inner_angle = radians(180) - ich_outer_angle
function isotoxal_concave_hexagon_points(d) {
var points = [];
var outscribed_triangle_edge = 2 * (d.r * Math.sqrt(3) / 2)
var edge = (outscribed_triangle_edge / 2) / Math.sin(ich_outer_angle / 2)
var x = 0;
var y = -d.r;
var x_change = Math.sin(ich_inner_angle / 2) * edge
var y_change = Math.cos(ich_inner_angle / 2) * edge
points.push([x, y])
x += x_change
y += y_change
points.push([x, y])
var lx = outscribed_triangle_edge / 2 - x_change
var ly = Math.sin((radians(180) - ich_outer_angle) / 2 + ich_inner_angle) * edge
x += lx
y += ly
points.push([x, y])
x -= y_change
y -= x_change
points.push([x, y])
x -= y_change
y += x_change
points.push([x, y])
x += lx
y -= ly
points.push([x, y])
return points
}
var octagon_cr_to_a_ratio = 1 / (Math.sqrt(4 + 2 * Math.sqrt(2)) / 2);
var octagon_angle = (180 - 135) * (2 * Math.PI) / 360;
function octagon_points(d) {
var points = [];
// d.r is a circumradius here
var a = d.r * octagon_cr_to_a_ratio;
var x = -d.r + 1;
var y = d.r / 2;
for(var i = 0; i < 8; i++)
{
var angle = octagon_angle * (i + 1);
x += a * Math.sin(angle);
y += a * Math.cos(angle);
points.push([x, y])
}
return points
}
return {
isotoxal_concave_hexagon: isotoxal_concave_hexagon_points,
octagon: octagon_points
}
}
function init_edges(vis, edges)
{
links = vis.selectAll('.link')
.data(edges)
.enter().append('line')
.attr('class', 'link')
function kinase_site_with_mimp_effect(effect)
{
var effect_accessor = 'mimp_' + effect
return function(d)
{
var site = d.target;
if (site.type != types.site) return false;
var kinase = d.source
return (
(
kinase.type == types.kinase &&
site[effect_accessor].indexOf(kinase.name) !== -1
) ||
(
kinase.type == types.group &&
site[effect_accessor + '_family'].indexOf(kinase.name) !== -1
)
)
}
}
['losses', 'gains'].forEach(
function(effect)
{
var effect_accessor = 'mimp_' + effect
links
.filter(kinase_site_with_mimp_effect(effect))
.classed(effect + '-prediction', true)
// the link will be scaled linearly to the number of mimp loss
// predictions. This number will be always >= 1 (because we
// are working on such filtered subset of links)
.style('stroke-width', function(d){
var kinase = d.source
var accessor = effect_accessor
if(kinase.type == types.group){
accessor += '_family'
}
var site = d.target
var count = 0
var mimp = site[accessor]
for(var i = 0; i < mimp.length; i++)
{
count += (mimp[i] === kinase.name)
}
return count * 1.5
})
}
)
}
function init_tooltips()
{
var tooltip = Tooltip()
tooltip.init({
id: 'node',
template: function(node){
return nunjucks.render(
'node_tooltip.njk',
{
refseq: central_node.protein.refseq,
node: node,
types: types,
nodeURL: config.nodeURL
}
)
},
viewport: document.body,
callback: function(element){
$(element).find('.site-muts-table').bootstrapTable()
}
})
dispatch.on('networkmove', function(){
tooltip.moveToElement()
})
return tooltip
}
function init_nodes(vis, nodes_data, created_nodes)
{
tooltip = init_tooltips()
nodes = vis.selectAll('.node')
.data(nodes_data)
.enter().append('g')
.attr('class', 'node')
.call(force_manager.drag)
.on('click', node_click)
.on('dblclick', node_dbl_click)
.on('mouseover', function(d){ nodeHover(d, true) })
.on('mouseout', function(d){ nodeHover(d, false) })
// cancel other events (like pining the background)
// to allow nodes movement (by force.drag)
.on('mousedown', function(d) { d3.event.stopPropagation() })
.call(tooltip.bind)
function nodes_subset(type) {
return nodes.filter(is_of_type(type));
}
var kinase_nodes = nodes_subset(types.kinase);
var group_nodes = nodes_subset(types.group);
var central_nodes = nodes_subset(types.central);
var drug_nodes = nodes_subset(types.drug);
var site_nodes = nodes_subset(types.site);
var kinases_color_scale = create_color_scale(
[
0,
d3.max(created_nodes.standalone, function(d){
return d.protein ? d.protein.mutations_count : 0
}) || 0
],
['#ffffff', '#007FFF']
);
kinase_nodes
.append('circle')
.attr('r', function(d){ return d.r })
.attr('class', 'kinase protein-like shape')
var shapes = create_shapes()
drug_nodes
.append('polygon')
.attr('points', shapes.isotoxal_concave_hexagon)
.attr('class', 'drug shape')
group_nodes
.append('polygon')
.attr('points', shapes.octagon)
.attr('class', 'group shape')
central_nodes
.append('ellipse')
.attr('rx', function(d){ return d.r })
.attr('ry', function(d){ return d.r / 5 * 4 })
.attr('class', 'central protein-like shape')
nodes.selectAll('.protein-like')
.attr('fill', function(d){
if(d.protein)
return kinases_color_scale(d.protein.mutations_count)
})
site_nodes
.attr('class', function(d){
return 'node ' + d.impact
})
.append('rect')
.attr('width', function(d){ return d.size + 'px' })
.attr('height', function(d){ return d.size + 'px' })
.attr('class', 'site shape')
.attr('transform', function(d){ return 'translate(' + [-d.size / 2, -d.size / 2] + ')'} )
nodes
.append('text')
.attr('class', 'name')
.text(function(d){
//if(d.name.length > 9)
// return d.name.substring(0, 7) + '...'
return d.name
})
.style('font-size', function(d) {
if(d.type !== types.site)
return fitTextIntoCircle(d, this) + 'px'
})
.attr('y', function (d) {
if(d.type === types.drug)
return d.r * 0.8
})
site_nodes.selectAll('.name')
.style('font-size', '8px')
.attr('dy', '-0.5em')
site_nodes
.append('text')
.text(function(d) { return d.sequence.slice(4, -4) })
.style('font-size', '5.5px')
.attr('dy', '1.5em')
group_nodes
.append('text')
.attr('class', 'type')
.text(function(d){ return 'family ' + d.kinases.length + '/' + d.total_cnt })
.style('font-size', function(d) {
return fitTextIntoCircle(d, this) * 0.5 + 'px'
})
.attr('dy', function(d) {
return fitTextIntoCircle(d, this) * 0.35 + 'px'
})
}
function init()
{
svg = prepareSVG(config.element)
var vis = svg.append('g')
zoom = (choose_best_zoom())()
zoom.init({
element: svg,
inner_element: vis,
min: config.min_zoom,
max: config.max_zoom,
viewport: svg.node().parentNode,
on_move: function(event) { dispatch.networkmove(event) }
})
// TODO: test this, deduplicate
// we don't want to close tooltips after panning (which is set to emit
// stopPropagation on start what allows us to detect end-of-panning events)
svg.on('click', function(){
if(d3.event.defaultPrevented) d3.event.stopPropagation()
})
resize()
var created_nodes = create_nodes(config.data);
var nodes_data = created_nodes.all;
placeNodes(created_nodes.sites, created_nodes.standalone, created_nodes.kinases);
force_manager = ForceManager({
size: zoom.viewport_to_canvas([config.width, config.height])
})
force_manager.update_force_affected_nodes(nodes_data, edges)
force_manager.drag()
.on('dragstart', function(d){
d3.event.sourceEvent.stopPropagation()
d.fixed = true
})
init_edges(vis, edges)
init_nodes(vis, nodes_data, created_nodes)
force_manager.start()
// collapse all groups immediately (time=0)
groups.forEach(function(group){ switchGroupState(group, false, 0) });
$(window).on('resize', resize)
// fast, steady initialization
force_manager.settle_force(10);
var ticker = create_ticker();
force_manager.on('tick', ticker);
ticker();
public_space.zoom_fit(0) // grasp everything without animation (avoids repeating zoom lags if they occur)
window.setTimeout(public_space.zoom_fit, 1000)
config.onload()
}
var public_space = {
init: function(user_config)
{
if(!user_config)
{
init()
}
else
{
configure(user_config, public_space.init)
}
},
zoom_in: function(){
zoom.set_zoom(zoom.get_zoom() * 1.25)
},
zoom_out: function(){
zoom.set_zoom(zoom.get_zoom() / 1.25)
},
show_central: function(animation_speed)
{
var radius = get_max_radius();
focusOn(central_node, radius, animation_speed)
},
zoom_fit: function(animation_speed){
zoom.fit_contents(animation_speed)
},
destroy: function()
{
svg.remove()
},
resize: function() {
resize();
force_manager.set_size(zoom.viewport_to_canvas([config.width, config.height]))
force_manager.start();
}
}
return public_space
}