import d3 from 'd3'; import Circos from '../packages/circosJS@1.7.0'; import _ from 'underscore'; import Grapher from './grapher'; import * as Helpers from './visualisation_helpers'; import Utils from './utils'; class Graph { static name() { return 'Queries and their top hits: chord diagram'; } static className() { return 'circos'; } static collapseId(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; this.initiate(); } initiate() { // this.width = 700; this.width = this.svgContainer.width(); this.height = 600; this.innerRadius = 200; this.outerRadius = 230; this.query_arr = []; this.hit_arr = []; this.layout_arr = []; this.chords_arr = []; // this.max_length = 0; this.hsp_count = 50; this.denominator = 1; this.spacing = 20; this.labelSpacing = 10; var suffixes = { amino_acid: 'aa', nucleic_acid: 'bp' }; this.construct_layout(); this.iterator_for_edits(); this.hit_arr = _.uniq(this.hit_arr); this.handle_spacing(); var prefix = d3.formatPrefix(this.max_length); this.suffix = ' ' + prefix.symbol + suffixes[this.seq_type.subject_seq_type]; if (prefix.symbol == 'k') { this.denominator = 1000; } else if (prefix.symbol == 'M') { this.denominator = 1000000; this.spacing = 1000000; this.labelSpacing = 200000; } else if (prefix.symbol == 'g') { this.denominator = 1000000000; } d3.select(this.svgContainer[0]) .insert('svg', ':first-child') .attr('class', 'circosContainer'); this.create_instance(this.svgContainer, this.width, this.height); if (this.chords_arr.length && this.layout_arr.length) { this.instance_render(); } else { this.render_error(); } 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 hsp_count = 0; var query_count = 0; 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(function (query) { if (query_count < 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); var hit_details = _.map( query.hits, _.bind(function (hit) { if (hit.number < num_hits) { var hsp_details = _.map( hit.hsps, _.bind(function (hsp) { 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); } var item3 = [ 'Query_' + this.clean_id(query.id), hsp.qstart, hsp.qend, 'Hit_' + this.clean_id(hit.id), hsp.sstart, hsp.send, hit.number, hsp, ]; this.chords_arr.push(item3); return hsp; }, this) ); return hit; } }, this) ); } this.query_arr.push(query.id); return query; }, this) ); } // 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 { usePalette: true, 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: this.suffix, 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('Circos looks great with less 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('data-toggle', 'tooltip') .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('data-toggle', 'tooltip') .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('data-toggle', 'tooltip') .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 += `
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; }); }); $('[data-toggle="tooltip"]').tooltip({ placement: 'top', container: 'body', html: 'true', delay: 0, 'white-space': 'nowrap', }); } 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('data-toggle', 'tooltip') .attr('title', function (d) { return d.evalue; }) .attr('x', 1) .attr('width', 30) .attr('height', 20); // .attr('fill','#43ff21'); var scale = d3.scale.linear().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);