wurmlab/sequenceserver

View on GitHub
public/js/length_distribution.js

Summary

Maintainability
A
3 hrs
Test Coverage
B
85%
import * as d3 from 'd3';
import _ from 'underscore';
import Grapher from 'grapher';
import * as Helpers from './visualisation_helpers';

/**
 * Renders Length Distribution of all hits per query
 */

class Graph {
    static canCollapse() {
        return true;
    }

    static name() {
        return 'Length distribution of matching hit sequences';
    }

    static className() {
        return 'length-distribution';
    }

    static graphId(props) {
        return 'length_'+props.query.number;
    }

    static dataName(props) {
        return 'length-distribution-'+props.query.id;
    }

    constructor($svg_container, props) {
        this.query = props.query;
        this._seq_type = Helpers.get_seq_type(props.algorithm);
        this.svg_container = $svg_container;
        if (props.algorithm == 'blastx') {
            this.query_length = this.query.length / 3;
        } else if (props.algorithm == 'tblastn') {
            this.query_length = this.query.length * 3;
        } else {
            this.query_length = this.query.length;
        }
        this._margin = {top: 30, right: 25, bottom: 55, left: 12.5};
        this.initiate($svg_container.width(), $svg_container.height());
    }

    initiate(width, height) {
        this._width = width - this._margin.left - this._margin.right;
        this._height = height - this._margin.top - this._margin.bottom;
        this.svg = d3.select(this.svg_container[0]).insert('svg', ':first-child')
            .attr('width', this._width + this._margin.right + this._margin.left)
            .attr('height', this._height + this._margin.top + this._margin.bottom)
            .append('g')
            .attr('transform','translate('+this._margin.left+','+this._margin.top+')');
        this.hit_lengths();
        this.define_scale_and_bins();
        this.update_data();
        this.draw();
    }

    draw() {
        this.draw_rectangles();
        this.draw_query_line();
        this.draw_axes();
    }

    define_scale_and_bins() {
        this._scale_x = d3.scaleLinear()
            .domain([
                0,
                (d3.max([this.query_length, d3.max(this._data)]) * 1.01)
            ]).nice()
            .range([0, this._width]);
        this._bins = d3.bin()
            .domain(this._scale_x.domain())
            .thresholds(this._scale_x.ticks(50))(this._data);
        this._scale_y = d3.scaleLinear()
            .domain([0, d3.max(this._bins, function(d) { return d.length; })])
            .range([this._height, 0]).nice();
    }

    hit_lengths() {
        this._data = _.map(this.query.hits, _.iteratee('length'));
    }

    setupResponsiveness() {
        var currentWidth = $(window).width();
        console.log('cureent '+currentWidth);
        var debounced_draw = _.debounce(_.bind(function () {
            if (currentWidth != $(window).width()) {
                console.log('redraw initiated '+this._height);
                this.draw();
                currentWidth = $(window).width();
            }
        }, this), 125);
        $(window).resize(debounced_draw);
    }

    tick_formatter(seq_type) {
        var ticks = this._scale_x.ticks();
        var prefix = d3.format('~s');
        var suffixes = {amino_acid: 'aa', nucleic_acid: 'bp'};
        return function (d) {
            if (d === 0) { return ; }
            if (_.indexOf(ticks,d) >= 0) {
                if (suffixes[seq_type] == 'aa') {
                    return `${d} ${suffixes[seq_type]}`;
                } else {
                    return `${prefix(d)}${suffixes[seq_type]}`.replace(/([a-zA-Z]+)/, ' $1');
                }
            } else {
                return ;
            }
        };
    }

    update_data() {
        var self = this;
        var data2 = [];
        this._bins.map(function (bin) {
            var inner_data = [];
            bin.reverse();
            var y0 = bin.length;
            bin.map(function (d,i) {
                var y1 = bin.length - (i+1);
                var len_index = _.findIndex(self.query.hits, {length: d});
                var evalue = self.query.hits[len_index].hsps[0].evalue;
                var item = {
                    value: d,
                    id: self.query.hits[len_index].id,
                    evalue: evalue,
                    url: '#Query_'+self.query.number+'_hit_'+self.query.hits[len_index].number,
                    y0: y0,
                    y1: y0 += (y1 - y0),
                    color: Helpers.get_colors_for_evalue(evalue,self.query.hits)
                };
                inner_data.push(item);
            });
            var item = {data: inner_data, x: bin.x0, dx: bin.x1, length: bin.length};
            data2.push(item);
        });
        this._update_data = data2;
    }

    draw_rectangles() {
        var self = this;
        var bar = this.svg.selectAll('.bar')
            .data(this._update_data)
            .enter().append('g')
            .attr('class', 'g')
            .attr('transform', function(d) {
                return 'translate('+(self._scale_x(d.x)+self._margin.left)+',0)';
            });

        bar.selectAll('rect')
            .data(function (d) { return d.data; })
            .enter().append('a')
            .attr('xlink:href', function(i) { return i.url; })
            .append('rect')
            .attr('class','bar')
            .attr('title', function(i) {
                return i.id+' '+'<br>E value: '+Helpers.prettify_evalue(i.evalue)+'<br>Length: '+i.value;
            })
            .attr('x', 1)
            .attr('y', function(i) { return (self._scale_y(i.y0)); })
            .attr('width', self._scale_x(this._bins[1].x0) - self._scale_x(this._bins[0].x0) - 1)
            .attr('height', function (i) { return self._scale_y(i.y1) - self._scale_y(i.y0); })
            .attr('fill', function(i) {
                return i.color;
            });
    }

    draw_query_line() {
        var query_line = this.svg.append('g')
            .attr('class','query_line')
            .attr('transform','translate('+(this._margin.left+this._scale_x(this.query_length))+',0)');

        query_line.append('rect')
            .attr('x',1)
            .attr('class','bar')
            .attr('width',4)
            .attr('height',this._height)
            .style('fill','#c74f14');

        query_line.append('text')
            .attr('dy', '0.75em')
            .attr('y', -10)
            .attr('x', 2)
            .attr('text-anchor','start')
            .text('Query')
            .style('fill','#000')
            .attr('transform','rotate(-45)');
    }

    draw_axes() {
        var space, len;
        len = this._scale_y.ticks().length;
        if (len >= 5) {
            space = 5;
        } else {
            space = len;
        }
        var formatter = this.tick_formatter(this._seq_type.subject_seq_type);
        var x_axis = d3.axisTop(this._scale_x)
            .ticks(50)
            .tickFormat(formatter);
        var y_axis = d3.axisLeft(this._scale_y)
            .tickValues(this._scale_y.ticks(space))
            .tickSizeOuter(0)
            .tickFormat(function (e) {
                if (Math.floor(e) != e) {
                    return ;
                }
                return e;
            });
        var ticks = this._scale_y.ticks();
        for (var i in ticks) {
            if (ticks[i] % 1 != 0) {
                y_axis.tickValues(d3.range(0, d3.max(this._bins, function(d) { return d.length; })+1));
                break;
            }
        }
        var self = this;
        var xContainer = this.svg.append('g')
            .attr('class', 'axis axis--x')
            .attr('transform', 'translate('+this._margin.left+','+this._height+')')
            .call(x_axis);

        xContainer.selectAll('line').attr('y2',function (d) {
            var ticks = self._scale_x.ticks();
            if (_.indexOf(ticks, d) >= 0) {
                return 7;
            } else {
                return 4;
            }
        });

        xContainer.selectAll('text').style('text-anchor','end')
            .attr('x', '-8px')
            .attr('y', '3px')
            .attr('dy', '0')
            .attr('transform','rotate(-90)');

        this.svg.append('g')
            .attr('class','axis axis--y')
            .attr('transform','translate('+this._margin.left+',0)')
            .call(y_axis);
    }
}

var LengthDistribution = Grapher(Graph);
export default LengthDistribution;