wurmlab/sequenceserver

View on GitHub
public/js/circos.js

Summary

Maintainability
F
6 days
Test Coverage
import * as d3 from 'd3';
import Circos from './circosjs';
import _ from 'underscore';

import Grapher from 'grapher';
import * as Helpers from './visualisation_helpers';
import Utils from './utils';

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

    static name() {
        return 'Chord diagram of queries and their top hits';
    }

    static className() {
        return 'circos';
    }

    static graphId(props) {
        return 'circos-collapse';
    }

    static dataName(props) {
        return 'Circos-visualisation';
    }

    constructor($svgContainer, props) {
        this.queries = [];
        props.queries.forEach((query) => {
            if (query.hits.length > 0) {
                this.queries.push(query);
            }
        });
        this.svgContainer = $svgContainer;
        this.seq_type = Helpers.get_seq_type(props.program);
        this.algorithm = props.program;
        // Initialize dimensions
        this.width = this.svgContainer.width();
        this.height = 600;
        this.innerRadius = 200;
        this.outerRadius = 230;

        // Initialize data arrays
        this.query_arr = [];
        this.hit_arr = [];
        this.layout_arr = [];
        this.chords_arr = [];

        // Initialize other properties
        this.hsp_count = 50;
        this.denominator = 1;
        this.spacing = 20;
        this.labelSpacing = 10;
        this.initiate();
    }

    initiate() {
        // Call initialization methods
        this.construct_layout();
        this.iterator_for_edits();

        // Remove duplicate hits
        this.hit_arr = _.uniq(this.hit_arr);

        // Handle spacing
        this.handle_spacing();

        // Insert the circos container
        d3.select(this.svgContainer[0])
            .insert('div', ':first-child')
            .attr('class', 'circosContainer');

        // Create the instance
        this.create_instance(this.svgContainer, this.width, this.height);

        // Render the instance or show an error
        if (this.chords_arr.length && this.layout_arr.length) {
            this.instance_render();
        } else {
            this.render_error();
        }

        // Setup the tooltip
        this.setupTooltip();
        // this.drawLegend();
    }

    iterator_for_edits() {
        this.max_length = this.calculate_max_length();
        if (this.hit_arr.length > 10) {
            this.complex_layout_edits();
        }
    }

    // Generate both layout_arr and chords_arr with top hsps set by this.hsp_count
    construct_layout() {
        var num_karyotype = 32;
        var num_queries = this.queries.length;
        var x = Math.min(num_karyotype / 2, num_queries);
        var num_hits = (num_karyotype - x) / x;
        this.new_layout = [];
        this.data = _.map(this.queries, _.bind(this.processQuery, this, x, num_hits));
    }

    processQuery(x, num_hits, query) {
        if (this.query_arr.length < x) {
            var label = query.id;
            var len = query.length;
            var item1 = {
                len: len,
                color: '#8dd3c7',
                label: label,
                id: 'Query_' + this.clean_id(query.id),
                ori_id: label,
            };
            this.layout_arr.push(item1);
            _.map(query.hits, _.bind(this.processHit, this, num_hits, query));
        }
        this.query_arr.push(query.id);
        return query;
    }

    processHit(num_hits, query, hit) {
        if (hit.number < num_hits) {
            if (_.indexOf(this.hit_arr, hit.id) == -1) {
                var label = hit.id;
                var len = hit.length;
                this.hit_arr.push(hit.id);
                var item2 = {
                    len: len,
                    color: '#80b1d3',
                    label: label,
                    id: 'Hit_' + this.clean_id(hit.id),
                    ori_id: label,
                };
                this.layout_arr.push(item2);
            }
            _.map(hit.hsps, _.bind(this.processHSP, this, query, hit));
        }
        return hit;
    }

    processHSP(query, hit, hsp) {
        this.chords_arr.push([
            'Query_' + this.clean_id(query.id),
            hsp.qstart,
            hsp.qend,
            'Hit_' + this.clean_id(hit.id),
            hsp.sstart,
            hsp.send,
            hit.number,
            hsp,
        ]);
        return hsp;
    }

    // rearraging hit and query karyotypes to have all query in one place
    rearrange_new_layout() {
        _.each(
            this.new_layout,
            _.bind(function (obj) {
                var id = obj.id.slice(0, 3);
                if (id == 'Que') {
                    this.layout_arr.push(obj);
                }
            }, this)
        );
        _.each(
            this.new_layout,
            _.bind(function (obj) {
                var id = obj.id.slice(0, 3);
                if (id == 'Hit') {
                    this.layout_arr.push(obj);
                }
            }, this)
        );
    }

    // label edits along with deleting hits which are too small to display
    complex_layout_edits() {
        this.delete_from_layout = [];
        this.delete_from_chords = [];
        _.each(
            this.layout_arr,
            _.bind(function (obj, index) {
                var rel_length = (obj.len / this.max_length).toFixed(3);
                var label = obj.label;
                if (rel_length < 0.1 && obj.id.slice(0, 3) != 'Que') {
                    this.delete_from_layout.push(obj);
                    this.hit_arr.slice(_.indexOf(this.hit_arr, obj.label), 1); // corresponding delete from hit_arr
                }
            }, this)
        );
    }

    // get the chords_arr index based on hit or query id
    check_in_chords_arr(id, type, index) {
        var count = 0;
        _.each(
            this.chords_arr,
            _.bind(function (obj) {
                if (type == 'Que') {
                    if (obj[0] != id) {
                        count++;
                    }
                }
            }, this)
        );
        if (count == this.chords_arr.length) {
            console.log('no record found ' + id);
            this.delete_arr.push(index);
        }
    }

    // get index of hit_arr based on id
    find_index_of_hit(id) {
        var found;
        _.each(
            this.queries,
            _.bind(function (query) {
                _.each(
                    query.hits,
                    _.bind(function (hit) {
                        var check_id = 'Hit_' + this.clean_id(hit.id);
                        if (id == check_id) {
                            found = hit.id;
                        }
                    }, this)
                );
            }, this)
        );
        return _.indexOf(this.layout_arr, found);
    }

    edit_labels() {
        console.log('label edits');
        _.each(
            this.layout_arr,
            _.bind(function (obj) {
                var rel_length = (obj.len / this.max_length).toFixed(3);
                var label = obj.label;
                if (rel_length < 0.41) {
                    obj.label = '..';
                } else if (label.length > 10) {
                    obj.label = label.slice(0, 2) + '...';
                } else {
                    obj.label = obj.ori_id;
                }
            }, this)
        );
    }

    calculate_multipliers() {
        var sum_query_length = 0;
        var sum_hit_length = 0;
        _.each(
            this.query_arr,
            _.bind(function (id) {
                _.each(
                    this.data,
                    _.bind(function (query) {
                        if (id == query.id) {
                            sum_query_length += query.length;
                        }
                    }, this)
                );
            }, this)
        );

        _.each(
            this.data,
            _.bind(function (query) {
                _.each(
                    query.hits,
                    _.bind(function (hit) {
                        var index = _.indexOf(this.hit_arr, hit.id);
                        if (index >= 0) {
                            sum_hit_length += hit.length;
                        }
                    }, this)
                );
            }, this)
        );
        var mid_sum = (sum_query_length + sum_hit_length) / 2;
        console.log(
            'mid sum ' +
      mid_sum +
      ' hit_sum ' +
      sum_hit_length +
      ' query_sum ' +
      sum_query_length
        );
        this.query_multiplier = (mid_sum / sum_query_length).toFixed(3);
        this.hit_multiplier = (mid_sum / sum_hit_length).toFixed(3);
        console.log(
            'query ' + this.query_multiplier + ' hit ' + this.hit_multiplier
        );
    }

    handle_spacing() {
        if (this.max_length > 16000) {
            this.spacing = 200;
        } else if (this.max_length > 12000) {
            this.spacing = 150;
        } else if (this.max_length > 8000) {
            this.spacing = 100;
        } else if (this.max_length > 4000) {
            this.spacing = 75;
        } else if (this.max_length > 1800) {
            this.spacing = 50;
        }
    }

    calculate_max_length() {
        var max = 0;
        _.each(this.layout_arr, function (obj) {
            if (max < obj.len) {
                max = obj.len;
            }
        });
        return max;
    }

    clean_id(id) {
        return id.replace(/[^a-zA-Z0-9]/g, '');
    }

    create_instance(container, width, height) {
        this.instance = new Circos({
            container: '.circosContainer',
            width: width,
            height: height,
        });
        this.chord_layout();
        this.instance_layout();
    }

    chord_layout() {
        if (this.chords_arr.length > 32) {
            this.paletteSize = 32;
        } else {
            this.paletteSize = this.chords_arr.length;
        }
        return {
            colorPaletteSize: this.paletteSize,
            // color: 'rgb(0,0,0)',
            colorPalette: 'RdYlBu', // colors of chords based on last value in chords
            // tooltipContent: 'Hiten',
            opacity: 0.85, // add opacity to ribbons
        };
    }

    instance_layout() {
        return {
            innerRadius: this.innerRadius,
            outerRadius: this.outerRadius,
            cornerRadius: 1, // rounding at edges of karyotypes
            labels: {
                display: true,
                size: '10px',
                radialOffset: 10,
            },
            ticks: {
                display: true,
                spacing: this.spacing, // the ticks values to display
                labelSpacing: this.labelSpacing, // ticks value apper in interval
                labelDenominator: this.denominator, // divide the value by this value
                labelSuffix: '',
                labelSize: '10px',
                majorSpacing: this.labelSpacing, // major ticks apper in interval
                size: {
                    minor: 0, // to remove minor ticks
                    major: 4,
                },
            },
        };
    }

    instance_render() {
        this.instance.layout(this.instance_layout(), this.layout_arr);
        this.instance.chord('chord1', this.chord_layout(), this.chords_arr);
        this.instance.render();
    }

    render_error() {
        this.svgContainer.find('svg').remove();
        this.svg = d3
            .select(this.svgContainer[0])
            .insert('svg', ':first-child')
            .attr('width', this.svgContainer.width())
            .attr('height', this.svgContainer.height())
            .append('g')
            .attr('class', 'circos-error')
            .attr(
                'transform',
                'translate(' +
        this.svgContainer.width() / 2 +
        ',' +
        this.svgContainer.height() / 2 +
        ')'
            )
            .append('text')
            .attr('text-anchor', 'start')
            .attr('dy', '-0.25em')
            .attr('x', -175)
            .style('font-size', '14px')
            .text('Chord diagram looks great with fewer than 16 queries');
    }

    layoutReset() {
        this.layoutHide = [];
        _.each(this.layout_arr, function (obj) {
            $('.' + obj.id).css('opacity', 1);
        });
    }

    chordsReset() {
        this.chordsHide = [];
        _.each(this.chords_arr, function (obj) {
            var slen = obj[1] + obj[2];
            var tlen = obj[4] + obj[5];
            $('#' + obj[0] + '_' + slen + '_' + obj[3] + '_' + tlen).show();
        });
    }

    chordsCheck(id, type) {
        _.each(
            this.chords_arr,
            _.bind(function (obj, index) {
                if (type == 'Que') {
                    if (obj[0] == id) {
                        this.chordsHide.push(index);
                        this.layoutHide.push(obj[3]);
                    }
                }
                if (type == 'Hit') {
                    if (obj[3] == id) {
                        this.chordsHide.push(index);
                        this.layoutHide.push(obj[0]);
                    }
                }
            }, this)
        );
    }

    chordsClean() {
        _.each(
            this.chords_arr,
            _.bind(function (obj, index) {
                if (_.indexOf(this.chordsHide, index) == -1) {
                    var slen = obj[1] + obj[2];
                    var tlen = obj[4] + obj[5];
                    $('#' + obj[0] + '_' + slen + '_' + obj[3] + '_' + tlen).hide();
                }
            }, this)
        );
    }

    layoutClean() {
        _.each(
            this.layout_arr,
            _.bind(function (obj, index) {
                if (_.indexOf(this.layoutHide, obj.id) == -1) {
                    $('.' + obj.id).css('opacity', 0.1);
                }
            }, this)
        );
    }

    setupTooltip() {
        var selected = {};
        $('.circos-distribution').on(
            'click',
            _.bind(function (event) {
                event.stopPropagation();
                this.layoutReset();
                this.chordsReset();
                selected = {};
            }, this)
        );
        _.each(
            this.query_arr,
            _.bind(function (id, index) {
                this.chordsHide = [];
                this.layoutHide = [];
                if (id) {
                    $('.circos .Query_' + this.clean_id(id))
                        .attr('title', id)
                        .on(
                            'click',
                            _.bind(function (event) {
                                event.stopPropagation();
                                if (selected[index] != id) {
                                    selected[index] = id;
                                    var cleaned_id = 'Query_' + this.clean_id(id);
                                    this.layoutHide.push(cleaned_id);
                                    this.chordsCheck(cleaned_id, 'Que');
                                    this.chordsClean();
                                    this.layoutClean();
                                } else {
                                    selected[index] = 0;
                                    this.layoutReset();
                                    this.chordsReset();
                                }
                            }, this)
                        );
                }
            }, this)
        );
        _.each(
            this.hit_arr,
            _.bind(function (id, index) {
                this.chordsHide = [];
                this.layoutHide = [];
                if (id) {
                    $('.circos .Hit_' + this.clean_id(id))
                        .attr('title', id)
                        .on(
                            'click',
                            _.bind(function (event) {
                                event.stopPropagation();
                                if (selected[index] != id) {
                                    selected[index] = id;
                                    var cleaned_id = 'Hit_' + this.clean_id(id);
                                    this.layoutHide.push(cleaned_id);
                                    this.chordsCheck(cleaned_id, 'Hit');
                                    this.chordsClean();
                                    this.layoutClean();
                                } else {
                                    selected[index] = 0;
                                    this.layoutReset();
                                    this.chordsReset();
                                }
                            }, this)
                        );
                }
            }, this)
        );
        var algorithm = this.algorithm;
        _.each(this.chords_arr, function (obj) {
            $('#' + obj[0] + '_' + obj[3])
                .attr('title', function () {
                    // E value and identity.
                    var alt_tooltip =
            'E value: ' +
            Helpers.prettify_evalue(obj[7].evalue) +
            `, Identities: ${Utils.inPercentage(
                obj[7].identity,
                obj[7].length
            )}`;
                    // Positives (for protein alignment).
                    if (algorithm != 'blastn') {
                        alt_tooltip += `<br>Positives: ${Utils.inPercentage(
                            obj[7].positives,
                            obj[7].length
                        )}`;
                    }
                    // Gaps. My understanding is that identities and gaps should add up to 100%.
                    alt_tooltip += `, Gaps: ${Utils.inPercentage(
                        obj[7].gaps,
                        obj[7].length
                    )}`;
                    return alt_tooltip;
                });
        });
        $('.circos').tooltip({
            position: {
                my: 'left+3 bottom-3',
                at: 'right bottom',
                using: function(position, feedback) {
                  $(this).css(position);
                  $('<div>')
                    .addClass('arrow')
                    .addClass(feedback.vertical)
                    .addClass(feedback.horizontal)
                    .appendTo(this);
                }
            },
            items: '.chord1 path, .cs-layout g',
            show: false,
            hide: false,
            content: function() {
                var title = $(this).attr('title');
                if (!title) return false;
                var parsedHTML = $.parseHTML(title);
                return parsedHTML;
            }
        });
    }

    ratioCalculate(value, min, max, scope, reverse, logScale) {
        var fraction, scaleLogBase, x;
        scaleLogBase = logScale ? 2.3 : 1;
        if (
            min === max ||
      (value === min && !reverse) ||
      (value === max && reverse)
        ) {
            return 0;
        }
        if (value === max || (value === min && reverse)) {
            return scope - 1;
        }
        fraction = (value - min) / (max - min);
        x = Math.exp((1 / scaleLogBase) * Math.log(fraction));
        if (reverse) {
            x = 1 - x;
        }
        return Math.floor(scope * x);
    }

    drawLegend() {
        this.ratioHSP = [];
        _.each(
            this.chords_arr,
            _.bind(function (obj) {
                var item = { number: obj[6], evalue: obj[7].evalue };
                this.ratioHSP.push(item);
            }, this)
        );
        var min = d3.min(this.ratioHSP, function (d) {
            return d.number;
        });
        var max = d3.max(this.ratioHSP, function (d) {
            return d.number;
        });
        console.log('chords_arr ' + this.chords_arr.length);
        console.log('ratioHSP test ' + this.ratioHSP.length);
        console.log('paletteSize ' + this.paletteSize);
        console.log('min ' + min + ' max ' + max);
        this.legend = d3
            .select(this.svgContainer[0])
            .insert('svg', ':first-child')
            .attr('height', 20)
            .attr('width', this.ratioHSP.length * 30)
            .attr('transform', 'translate(10, 10)')
            .append('g')
            .attr('class', 'RdYlBu')
            .attr('transform', 'translate(10, 0)');

        var bar = this.legend
            .selectAll('.bar')
            .data(this.ratioHSP)
            .enter()
            .append('g')
            .attr('class', 'g')
            .attr('transform', function (d, i) {
                return 'translate(' + i * 30 + ',0)';
            })
            .append('rect')
            .attr(
                'class',
                _.bind(function (d, i) {
                    var s = this.ratioCalculate(
                        d.number,
                        min,
                        max,
                        this.paletteSize,
                        false,
                        false
                    );
                    console.log('calc ratio ' + s);
                    return 'q' + s + '-' + this.paletteSize;
                }, this)
            )
            .attr('title', function (d) {
                return d.evalue;
            })
            .attr('x', 1)
            .attr('width', 30)
            .attr('height', 20);
        // .attr('fill','#43ff21');

        var scale = d3.scaleLinear().domain([0, 250]).range([0, 100]);

        // this.legend.append('rect')
        //     .attr('x', 7*14)
        //     .attr('width', 2*10)
        //     .attr('height', 10)
        //     .attr('fill','#43ff21');
        //
        // this.legend.append('text')
        //     .attr('class','text-legend')
        //     .attr('transform','translate('+10+',0)')
        //     .attr('x',6*14)
        //     .text('Weaker Hits');
        //
        // this.legend.append('text')
        //     .attr('class','text-legend')
        //     .attr('transform','translate('+10+',0)')
        //     .attr('x',9*14)
        //     .text('Stronger Hits');

        // bar.selectAll('rect')

    // this.legend.append('rect')
    //     .attr('x',1)
    //     .attr('width', 10)
    //     .attr('height', 10)
    //     .attr('fill','#232323');
    }
}

export default Grapher(Graph);