public/js/circos.js
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);