Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering performance improvements #208

Merged
merged 15 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions esm/components/data_layer/annotation_track.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ class AnnotationTrack extends BaseDataLayer {
}).attr('width', (d, i) => {
const crds = _getX(d, i);
return (crds[1] - crds[0]) + this.layout.hitarea_width / 2;
})
// Set up tooltips and mouse interaction
});

// Set up tooltips and mouse interaction
this.svg.group
.call(this.applyBehaviors.bind(this));

// Remove unused elements
Expand Down
15 changes: 10 additions & 5 deletions esm/components/data_layer/arcs.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Arcs extends BaseDataLayer {
.selectAll('path.lz-data_layer-arcs-hitarea')
.data(track_data, (d) => this.getElementId(d));

this.svg.group
.call(applyStyles, layout.style);

// Add new points as necessary
selection
.enter()
Expand All @@ -73,8 +76,7 @@ class Arcs extends BaseDataLayer {
.attr('id', (d) => this.getElementId(d))
.merge(selection)
.attr('stroke', (d, i) => this.resolveScalableParameter(this.layout.color, d, i))
.attr('d', (d, i) => _make_line(d))
.call(applyStyles, layout.style);
.attr('d', (d, i) => _make_line(d));

hitareas
.enter()
Expand All @@ -86,16 +88,19 @@ class Arcs extends BaseDataLayer {
.style('stroke-width', layout.hitarea_width)
.style('stroke-opacity', 0)
.style('stroke', 'transparent')
.attr('d', (d) => _make_line(d))
// Apply mouse behaviors to hitareas
.call(this.applyBehaviors.bind(this));
.attr('d', (d) => _make_line(d));

// Remove old elements as needed
selection.exit()
.remove();

hitareas.exit()
.remove();

// Apply mouse behaviors to arcs
this.svg.group
.call(this.applyBehaviors.bind(this));

return this;
}

Expand Down
47 changes: 34 additions & 13 deletions esm/components/data_layer/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ class BaseDataLayer {
this.layout_idx = null;

/**
* The unique identifier for this layer. Should be unique within this layer.
* The unique identifier for this layer. Should be unique within this panel.
* @public
* @member {String}
*/
this.id = null;

/**
* The fully qualified identifier for the data layer, prefixed by any parent or container elements.
* @type {string}
* @private
*/
this._base_id = null;

/**
* @protected
* @member {Panel}
Expand Down Expand Up @@ -240,21 +247,26 @@ class BaseDataLayer {
* Fetch the fully qualified ID to be associated with a specific visual element, based on the data to which that
* element is bound. In general this element ID will be unique, allowing it to be addressed directly via selectors.
* @protected
* @param {String|Object} element
* @param {Object} element
* @returns {String}
*/
getElementId (element) {
let element_id = 'element';
if (typeof element == 'string') {
element_id = element;
} else if (typeof element == 'object') {
const id_field = this.layout.id_field || 'id';
if (typeof element[id_field] == 'undefined') {
throw new Error('Unable to generate element ID');
}
element_id = element[id_field].toString().replace(/\W/g, '');
// Use a cached value if possible
const id_key = Symbol.for('lzID');
if (element[id_key]) {
return element[id_key];
}
return (`${this.getBaseId()}-${element_id}`).replace(/([:.[\],])/g, '_');

const id_field = this.layout.id_field || 'id';
if (typeof element[id_field] == 'undefined') {
throw new Error('Unable to generate element ID');
}
const element_id = element[id_field].toString().replace(/\W/g, '');

// Cache ID value for future calls
const key = (`${this.getBaseId()}-${element_id}`).replace(/([:.[\],])/g, '_');
element[id_key] = key;
return key;
}

/**
Expand Down Expand Up @@ -749,10 +761,14 @@ class BaseDataLayer {
* @returns {string} A dot-delimited string of the format <plot>.<panel>.<data_layer>
*/
getBaseId () {
if (this._base_id) {
return this._base_id;
}

if (this.parent) {
return `${this.parent_plot.id}.${this.parent.id}.${this.id}`;
} else {
return '';
return (this.id || '').toString();
}
}

Expand All @@ -775,6 +791,7 @@ class BaseDataLayer {
* @returns {BaseDataLayer}
*/
initialize() {
this._base_id = this.getBaseId();

// Append a container group element to house the main data layer group element and the clip path
const base_id = this.getBaseId();
Expand Down Expand Up @@ -1205,6 +1222,10 @@ class BaseDataLayer {
};
const self = this;
return function(element) {
// This method may be used on two kinds of events: directly attached, or bubbled.
// D3 doesn't natively support bubbling very well; if no data is bound on the currentTarget, check to see
// if there is data available at wherever the event was initiated from
element = element || d3.select(d3.event.target).datum();

// Do nothing if the required control and shift key presses (or lack thereof) doesn't match the event
if (requiredKeyStates.ctrl !== !!d3.event.ctrlKey || requiredKeyStates.shift !== !!d3.event.shiftKey) {
Expand Down
12 changes: 7 additions & 5 deletions esm/components/data_layer/forest.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,17 @@ class Forest extends BaseDataLayer {
.attr('transform', transform)
.attr('fill', fill)
.attr('fill-opacity', fill_opacity)
.attr('d', shape)
// Apply behaviors to points
.on('click.event_emitter', (element_data) => {
this.parent.emit('element_clicked', element_data, true);
}).call(this.applyBehaviors.bind(this));
.attr('d', shape);

// Remove old elements as needed
points_selection.exit()
.remove();

// Apply behaviors to points
this.svg.group
.on('click.event_emitter', (element_data) => {
this.parent.emit('element_clicked', element_data, true);
}).call(this.applyBehaviors.bind(this));
}
}

Expand Down
113 changes: 47 additions & 66 deletions esm/components/data_layer/genes.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,12 @@ class Genes extends BaseDataLayer {
}
}

// Stash parent references on all genes, trascripts, and exons
// Stash parent references on all genes, transcripts, and exons
item.parent = this;
item.transcripts.map((d, t) => {
item.transcripts[t].parent = item;
item.transcripts[t].exons.map((d, e) => item.transcripts[t].exons[e].parent = item.transcripts[t]);
});

});
return this;
}
Expand All @@ -209,7 +208,7 @@ class Genes extends BaseDataLayer {
// Apply filters to only render a specified set of points
const track_data = this._applyFilters();
this.assignTracks(track_data);
let width, height, x, y;
let height;

// Render gene groups
const selection = this.svg.group.selectAll('g.lz-data_layer-genes')
Expand All @@ -227,10 +226,7 @@ class Genes extends BaseDataLayer {
const bboxes = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-data_layer-genes-statusnode')
.data([gene], (d) => data_layer.getElementStatusNodeId(d));

width = (d) => d.display_range.width;
height = data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;
x = (d) => d.display_range.start;
y = (d) => ((d.track - 1) * data_layer.getTrackHeight());

bboxes.enter()
.append('rect')
Expand All @@ -239,41 +235,35 @@ class Genes extends BaseDataLayer {
.attr('id', (d) => data_layer.getElementStatusNodeId(d))
.attr('rx', data_layer.layout.bounding_box_padding)
.attr('ry', data_layer.layout.bounding_box_padding)
.attr('width', width)
.attr('width', (d) => d.display_range.width)
.attr('height', height)
.attr('x', x)
.attr('y', y);
.attr('x', (d) => d.display_range.start)
.attr('y', (d) => ((d.track - 1) * data_layer.getTrackHeight()));

bboxes.exit()
.remove();

// Render gene boundaries
const boundary_fill = (d, i) => self.resolveScalableParameter(self.layout.color, d, i);
const boundary_stroke = (d, i) => self.resolveScalableParameter(self.layout.stroke, d, i);
const boundaries = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-boundary')
.data([gene], (d) => `${d.gene_name}_boundary`);

width = (d) => data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start);
height = 1;
x = (d) => data_layer.parent.x_scale(d.start);
y = function(d) {
return ((d.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size
+ data_layer.layout.label_exon_spacing
+ (Math.max(data_layer.layout.exon_height, 3) / 2);
};

boundaries.enter()
.append('rect')
.attr('class', 'lz-data_layer-genes lz-boundary')
.merge(boundaries)
.attr('width', width)
.attr('width', (d) => data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start))
.attr('height', height)
.attr('x', x)
.attr('y', y)
.style('fill', boundary_fill)
.style('stroke', boundary_stroke);
.attr('x', (d) => data_layer.parent.x_scale(d.start))
.attr('y', (d) => {
return ((d.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size
+ data_layer.layout.label_exon_spacing
+ (Math.max(data_layer.layout.exon_height, 3) / 2);
})
.style('fill', (d, i) => self.resolveScalableParameter(self.layout.color, d, i))
.style('stroke', (d, i) => self.resolveScalableParameter(self.layout.stroke, d, i));

boundaries.exit()
.remove();
Expand All @@ -282,60 +272,52 @@ class Genes extends BaseDataLayer {
const labels = d3.select(this).selectAll('text.lz-data_layer-genes.lz-label')
.data([gene], (d) => `${d.gene_name}_label`);

x = (d) => {
if (d.display_range.text_anchor === 'middle') {
return d.display_range.start + (d.display_range.width / 2);
} else if (d.display_range.text_anchor === 'start') {
return d.display_range.start + data_layer.layout.bounding_box_padding;
} else if (d.display_range.text_anchor === 'end') {
return d.display_range.end - data_layer.layout.bounding_box_padding;
}
};
y = (d) => ((d.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size;

labels.enter()
.append('text')
.attr('class', 'lz-data_layer-genes lz-label')
.merge(labels)
.attr('text-anchor', (d) => d.display_range.text_anchor)
.text((d) => (d.strand === '+') ? `${d.gene_name}→` : `←${d.gene_name}`)
.style('font-size', gene.parent.layout.label_font_size)
.attr('x', x)
.attr('y', y);
.attr('x', (d) => {
if (d.display_range.text_anchor === 'middle') {
return d.display_range.start + (d.display_range.width / 2);
} else if (d.display_range.text_anchor === 'start') {
return d.display_range.start + data_layer.layout.bounding_box_padding;
} else if (d.display_range.text_anchor === 'end') {
return d.display_range.end - data_layer.layout.bounding_box_padding;
}
})
.attr('y', (d) => ((d.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size
);

labels.exit()
.remove();

// Render exon rects (first transcript only, for now)
// Exons: by default color on gene properties for consistency with the gene boundary track- hence color uses d.parent.parent
const exon_fill = (d, i) => self.resolveScalableParameter(self.layout.color, d.parent.parent, i);
const exon_stroke = (d, i) => self.resolveScalableParameter(self.layout.stroke, d.parent.parent, i);

const exons = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-exon')
.data(gene.transcripts[gene.parent.transcript_idx].exons, (d) => d.exon_id);

width = (d) => data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start);
height = data_layer.layout.exon_height;
x = (d) => data_layer.parent.x_scale(d.start);
y = function() {
return ((gene.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size
+ data_layer.layout.label_exon_spacing;
};

exons.enter()
.append('rect')
.attr('class', 'lz-data_layer-genes lz-exon')
.merge(exons)
.style('fill', exon_fill)
.style('stroke', exon_stroke)
.attr('width', width)
.style('fill', (d, i) => self.resolveScalableParameter(self.layout.color, d.parent.parent, i))
.style('stroke', (d, i) => self.resolveScalableParameter(self.layout.stroke, d.parent.parent, i))
.attr('width', (d) => data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start))
.attr('height', height)
.attr('x', x)
.attr('y', y);
.attr('x', (d) => data_layer.parent.x_scale(d.start))
.attr('y', () => {
return ((gene.track - 1) * data_layer.getTrackHeight())
+ data_layer.layout.bounding_box_padding
+ data_layer.layout.label_font_size
+ data_layer.layout.label_exon_spacing;
});

exons.exit()
.remove();
Expand All @@ -344,24 +326,18 @@ class Genes extends BaseDataLayer {
const clickareas = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-clickarea')
.data([gene], (d) => `${d.gene_name}_clickarea`);

width = (d) => d.display_range.width;
height = data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;
x = (d) => d.display_range.start;
y = (d) => ((d.track - 1) * data_layer.getTrackHeight());
clickareas.enter()
.append('rect')
.attr('class', 'lz-data_layer-genes lz-clickarea')
.merge(clickareas)
.attr('id', (d) => `${data_layer.getElementId(d)}_clickarea`)
.attr('rx', data_layer.layout.bounding_box_padding)
.attr('ry', data_layer.layout.bounding_box_padding)
.attr('width', width)
.attr('width', (d) => d.display_range.width)
.attr('height', height)
.attr('x', x)
.attr('y', y)
// Apply mouse behaviors & events to clickareas
.on('click.event_emitter', (element) => element.parent.parent.emit('element_clicked', element, true))
.call(data_layer.applyBehaviors.bind(data_layer));
.attr('x', (d) => d.display_range.start)
.attr('y', (d) => ((d.track - 1) * data_layer.getTrackHeight()));

// Remove old clickareas as needed
clickareas.exit()
Expand All @@ -371,6 +347,11 @@ class Genes extends BaseDataLayer {
// Remove old elements as needed
selection.exit()
.remove();

// Apply mouse behaviors & events to clickareas
this.svg.group
.on('click.event_emitter', (element) => this.parent.emit('element_clicked', element, true))
.call(this.applyBehaviors.bind(this));
}

_getTooltipPosition(tooltip) {
Expand Down
Loading