reimandlab/Visualistion-Framework-for-Genome-Mutations

View on GitHub
website/static/orbits.js

Summary

Maintainability
C
1 day
Test Coverage
var Orbits = function ()
{
    var nodes = null
    var central_node = null

    var by_node = {}
    var orbits = []

    var config = {
        order_by: 'r',  // new of data attribute to be used in placing nodes in orbits. The node with highest value of this attribute will be used to populate first place on the first orbit, then the next in order to populate second and so on until the orbit will be filled completely. Then the same will be applied to the second orbit and so on. Radius is the best choice in means of optimal place use, but if all nodes have the same radius it might be better idea to use sorting to encode something else.
        stroke: 2.2,
        spacing: 15, // the distance between orbits
        first_ring_scale: 1.5 // `first_ring_scale * central_node.radius` define radius of the first ring
    }

    function configure(new_config)
    {
        if(!new_config) return

        // Automatical configuration update:
        update_object(config, new_config)
    }

    function perimeter(R)
    {
        return 2 * Math.PI * R
    }

    function compareNodes(a, b)
    {
        if (a[config.order_by] < b[config.order_by])
          return -1
        if (a[config.order_by] > b[config.order_by])
            return 1
          return 0
    }

    // warning: using constructor pattern, not module
    function Orbit(number, initial_outer_radius)
    {
        this.number = number
        this.nodes_count = 0

        this.outer_radius = initial_outer_radius
        this.radius = null  // inner radius in pixels
        this.width = 0   // how much space takes the biggest node on this orbit / 2

        this.space_available = perimeter(this.outer_radius)    // space available on outer belt

        this.addNode = function(node)
        {
            by_node[node.name] = this.number
            this.nodes_count += 1
        }

        this.setDimensions = function(outer_radius)
        {
            this.radius = outer_radius - this.width
        }

        this.getSpaceRequiredForNewNode = function()
        {
            var angle = 2 * Math.asin(this.width / this.outer_radius)
            return this.outer_radius * angle
        }

        this.recalculateSpace = function(old_perimeter)
        {
            var percent_available = this.space_available / old_perimeter
            this.space_available = perimeter(this.outer_radius) * percent_available
        }
    }

    function newOrbit(initial_outer_radius)
    {
        var orbit = new Orbit(orbits.length, initial_outer_radius)
        orbits.push(orbit)
        return orbit
    }

    function calculateOrbits()
    {
        // radius of the first orbit, depends of the size of central protein
        var base_length = central_node.r * config.first_ring_scale

        var orbit = newOrbit(base_length)

        for(var i = 0; i < nodes.length; i++)
        {
            var node = nodes[i]
            var node_width = node.r + config.stroke

            if(node_width > orbit.width)
            {
                // recalculate width, radius & available space
                var old_perimeter = perimeter(orbit.outer_radius)

                // the outer belt will be larger - let's rescale the outer belt
                orbit.width = node_width
                orbit.outer_radius = base_length + orbit.width * 2
                orbit.recalculateSpace(old_perimeter)

            }
            var space_for_node = orbit.getSpaceRequiredForNewNode()
            // if it does not fit, lets move the next orbit, reset values
            if(orbit.space_available < space_for_node)
            {
                // save the orbit which is full
                orbit.setDimensions(orbit.outer_radius)

                // create new orbit
                base_length = orbit.outer_radius + config.spacing

                // move to the new orbit
                orbit = newOrbit(base_length + node_width * 2)
                orbit.width = node_width
                space_for_node = orbit.getSpaceRequiredForNewNode()
            }

            // finaly since it has to fit to the orbit right now, place it on the current orbit
            orbit.addNode(node)
            orbit.space_available -= space_for_node

            // (orbit.space_available >= 0) should always be true

        }
        orbit.setDimensions(orbit.outer_radius)
    }

    function getSlots()
    {
        // how many nodes should be placed on each of the available orbits
        var slots = new Array(orbits.length)
        for(var i = 0; i < orbits.length; i++)
            slots[i] = orbits[i].nodes_count
        return slots
    }

    var publicSpace = {
        init: function(orbiting_nodes, the_central_node, config)
        {
            nodes = orbiting_nodes
            central_node = the_central_node
            configure(config)
            // sort nodes by radius from the smallest
            nodes.sort(compareNodes)
            calculateOrbits()
        },
        getRadiusByNode: function(node)
        {
            return orbits[by_node[node.name]].radius
        },
        getOrbit: function(node)
        {
            return orbits[by_node[node.name]]
        },
        getMaximalRadius: function()
        {
            var orbit = orbits[orbits.length - 1]
            return orbit.radius + orbit.width
        },
        placeNodes: function()
        {
            var slots = getSlots()

            for(var i = 0; i < nodes.length; i++)
            {
                var node = nodes[i]

                var orbit_id = by_node[node.name]
                var orbit = orbits[orbit_id]

                var full_circle = Math.PI * 2

                var fraction_occupied = slots[orbit_id] / orbit.nodes_count
                var angle = full_circle * fraction_occupied

                node.x = orbit.radius * Math.cos(angle) + central_node.x
                node.y = orbit.radius * Math.sin(angle) + central_node.y

                slots[orbit_id] -= 1
            }
        }
    }

    return publicSpace

}