oncoprintjs
Version:
A data visualization for cancer genomic data.
422 lines (396 loc) • 14.7 kB
text/typescript
import svgfactory from './svgfactory';
import $ from 'jquery';
import OncoprintModel from './oncoprintmodel';
import { Rule, RuleWithId } from './oncoprintruleset';
function nodeIsVisible(node: HTMLElement) {
let ret = true;
while (node && node.tagName.toLowerCase() !== 'html') {
if ($(node).css('display') === 'none') {
ret = false;
break;
}
node = node.parentNode as HTMLElement;
}
return ret;
}
export default class OncoprintLegendView {
private $svg: JQuery<SVGElement>;
private rendering_suppressed = false;
private width: number;
private rule_set_label_config = {
weight: 'bold',
size: 12,
font: 'Arial',
};
private rule_label_config = {
weight: 'normal',
size: 12,
font: 'Arial',
};
private padding_after_rule_set_label = 10;
private padding_between_rules = 20;
private padding_between_rule_set_rows = 10;
constructor(
private $div: JQuery,
private base_width: number,
private base_height: number
) {
this.$svg = $(svgfactory.svg(200, 200)).appendTo(this.$div);
this.width = $div.width();
}
private renderLegend(
model: OncoprintModel,
target_svg?: SVGElement,
show_all?: boolean
) {
if (this.rendering_suppressed) {
return;
}
if (typeof target_svg === 'undefined') {
target_svg = this.$svg[0];
}
if (!nodeIsVisible((target_svg as any) as HTMLElement)) {
return;
}
$(target_svg).empty();
const defs = svgfactory.defs();
target_svg.appendChild(defs);
const everything_group = svgfactory.group(0, 0);
target_svg.appendChild(everything_group);
const rule_sets = model.getRuleSets();
let y = 0;
const rule_start_x = 200;
for (let i = 0; i < rule_sets.length; i++) {
if (rule_sets[i].exclude_from_legend && !show_all) {
continue;
}
const rules = model.getActiveRules(rule_sets[i].rule_set_id);
if (rules.length === 0) {
// dont render this ruleset into legend if no active rules
continue;
}
const rule_set_group = svgfactory.group(0, y);
everything_group.appendChild(rule_set_group);
(function addLabel() {
if (
typeof rule_sets[i].legend_label !== 'undefined' &&
rule_sets[i].legend_label.length > 0
) {
const label = svgfactory.text(
rule_sets[i].legend_label,
0,
0,
12,
'Arial',
'bold'
);
rule_set_group.appendChild(label);
svgfactory.wrapText(label, rule_start_x);
}
})();
let x = rule_start_x + this.padding_after_rule_set_label;
let in_group_y_offset = 0;
const labelSort = function(ruleA: RuleWithId, ruleB: RuleWithId) {
const labelA = ruleA.rule.legend_label;
const labelB = ruleB.rule.legend_label;
if (labelA && labelB) {
return labelA.localeCompare(labelB);
} else if (!labelA && !labelB) {
return 0;
} else if (!labelA) {
return -1;
} else if (!labelB) {
return 1;
}
return 0;
};
rules.sort(function(ruleA, ruleB) {
// sort, by legend_order, then alphabetically
const orderA = ruleA.rule.legend_order;
const orderB = ruleB.rule.legend_order;
if (
typeof orderA === 'undefined' &&
typeof orderB === 'undefined'
) {
// if neither have defined order, then sort alphabetically
return labelSort(ruleA, ruleB);
} else if (
typeof orderA !== 'undefined' &&
typeof orderB !== 'undefined'
) {
// if both have defined order, sort by order
if (orderA < orderB) {
return -1;
} else if (orderA > orderB) {
return 1;
} else {
// if order is same, sort alphabetically
return labelSort(ruleA, ruleB);
}
} else if (typeof orderA === 'undefined') {
if (orderB === Number.POSITIVE_INFINITY) {
return -1; // A comes before B regardless, if B is forced to end
} else {
//otherwise, A comes after B if B has defined order and A doesnt
return 1;
}
} else if (typeof orderB === 'undefined') {
if (orderA === Number.POSITIVE_INFINITY) {
return 1; // A comes after B regardless, if A is forced to end
} else {
// otherwise, A comes before B if A has defined order and B doesnt
return -1;
}
}
return 0;
});
for (let j = 0; j < rules.length; j++) {
const rule = rules[j].rule;
if (rule.exclude_from_legend) {
continue;
}
const group = this.ruleToSVGGroup(
rule,
model,
target_svg,
defs
);
group.setAttribute(
'transform',
'translate(' + x + ',' + in_group_y_offset + ')'
);
rule_set_group.appendChild(group);
if (x + group.getBBox().width > this.width) {
x = rule_start_x + this.padding_after_rule_set_label;
in_group_y_offset =
rule_set_group.getBBox().height +
this.padding_between_rule_set_rows;
group.setAttribute(
'transform',
'translate(' + x + ',' + in_group_y_offset + ')'
);
}
x += group.getBBox().width;
x += this.padding_between_rules;
}
y += rule_set_group.getBBox().height;
y += 3 * this.padding_between_rule_set_rows;
}
const everything_box = everything_group.getBBox();
this.$svg[0].setAttribute('width', everything_box.width.toString());
// add 10px to height to give room for rectangle stroke, which doesn't factor in accurately into the bounding box
// so that bounding boxes are too small to show the entire stroke (see https://github.com/cBioPortal/cbioportal/issues/3994)
this.$svg[0].setAttribute(
'height',
(everything_box.height + 10).toString()
);
}
private ruleToSVGGroup(
rule: Rule,
model: OncoprintModel,
target_svg: SVGElement,
target_defs: SVGDefsElement
) {
const root = svgfactory.group(0, 0);
const config = rule.getLegendConfig();
if (config.type === 'rule') {
const concrete_shapes = rule.apply(
config.target,
model.getCellWidth(true),
this.base_height
);
if (rule.legend_base_color) {
// generate backgrounds
const baseRect = svgfactory.rect(
0,
0,
model.getCellWidth(true),
this.base_height,
{
type: 'rgba',
value: rule.legend_base_color,
}
);
root.appendChild(baseRect);
}
// generate shapes
for (let i = 0; i < concrete_shapes.length; i++) {
root.appendChild(
svgfactory.fromShape(concrete_shapes[i], 0, 0)
);
}
if (typeof rule.legend_label !== 'undefined') {
const font_size = 12;
const text_node = svgfactory.text(
rule.legend_label,
model.getCellWidth(true) + 5,
this.base_height / 2,
font_size,
'Arial',
'normal'
);
target_svg.appendChild(text_node);
const height = text_node.getBBox().height;
text_node.setAttribute(
'y',
(
parseFloat(text_node.getAttribute('y')) -
height / 2
).toString()
);
target_svg.removeChild(text_node);
root.appendChild(text_node);
}
} else if (config.type === 'number') {
const num_decimal_digits = 2;
const display_range = config.range.map(function(x) {
const num_digit_multiplier = Math.pow(10, num_decimal_digits);
return (
Math.round(x * num_digit_multiplier) / num_digit_multiplier
);
});
root.appendChild(
svgfactory.text(
display_range[0].toString(),
0,
0,
12,
'Arial',
'normal'
)
);
root.appendChild(
svgfactory.text(
display_range[1].toString(),
50,
0,
12,
'Arial',
'normal'
)
);
const mesh = 100;
const points: [number, number][] = [];
let fill = null;
let linear_gradient = null;
if (config.range_type === 'NON_POSITIVE') {
fill = config.negative_color;
} else if (config.range_type === 'NON_NEGATIVE') {
fill = config.positive_color;
} else if (config.range_type === 'ALL') {
linear_gradient = svgfactory.linearGradient();
const offset =
(Math.abs(display_range[0]) /
(Math.abs(display_range[0]) + display_range[1])) *
100;
linear_gradient.appendChild(
svgfactory.stop(offset, config.negative_color)
);
linear_gradient.appendChild(
svgfactory.stop(offset, config.positive_color)
);
target_defs.appendChild(linear_gradient);
}
points.push([5, 20]);
for (let i = 0; i < mesh; i++) {
const t = i / mesh;
const h = config.interpFn(
(1 - t) * config.range[0] + t * config.range[1]
);
const height = 20 * h;
points.push([5 + (40 * i) / mesh, 20 - height]);
}
points.push([45, 20]);
root.appendChild(
svgfactory.path(points, fill, fill, linear_gradient)
);
} else if (config.type === 'gradient') {
const num_decimal_digits = 2;
const display_range = config.range.map(function(x) {
const num_digit_multiplier = Math.pow(10, num_decimal_digits);
return (
Math.round(x * num_digit_multiplier) / num_digit_multiplier
);
});
const gradient = svgfactory.gradient(config.colorFn);
const gradient_id = gradient.getAttribute('id');
target_defs.appendChild(gradient);
root.appendChild(
svgfactory.text(
display_range[0].toString(),
0,
0,
12,
'Arial',
'normal'
)
);
root.appendChild(
svgfactory.text(
display_range[1].toString(),
120,
0,
12,
'Arial',
'normal'
)
);
root.appendChild(
svgfactory.rect(30, 0, 60, 20, {
type: 'gradientId',
value: gradient_id,
})
);
}
return root;
}
public setWidth(w: number, model: OncoprintModel) {
this.width = w;
this.renderLegend(model);
}
public removeTrack(model: OncoprintModel) {
this.renderLegend(model);
}
public addTracks(model: OncoprintModel) {
this.renderLegend(model);
}
public setTrackData(model: OncoprintModel) {
this.renderLegend(model);
}
public setTrackImportantIds(model: OncoprintModel) {
this.renderLegend(model);
}
public shareRuleSet(model: OncoprintModel) {
this.renderLegend(model);
}
public setRuleSet(model: OncoprintModel) {
this.renderLegend(model);
}
public setTrackGroupLegendOrder(model: OncoprintModel) {
this.renderLegend(model);
}
public hideTrackLegends(model: OncoprintModel) {
this.renderLegend(model);
}
public showTrackLegends(model: OncoprintModel) {
this.renderLegend(model);
}
public suppressRendering() {
this.rendering_suppressed = true;
}
public releaseRendering(model: OncoprintModel) {
this.rendering_suppressed = false;
this.renderLegend(model);
}
public toSVGGroup(
model: OncoprintModel,
offset_x: number,
offset_y: number
) {
const root = svgfactory.group(offset_x || 0, offset_y || 0);
this.$svg.append(root);
this.renderLegend(model, root, true);
root.parentNode.removeChild(root);
return root;
}
}