\$\begingroup\$
\$\endgroup\$
I'm reasonably new to D3
, and I've put together a class based React
component which outputs a genomic alignment visually. The result looks like this:
The component is as follows:
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import * as d3 from 'd3';
const StyledSVG = styled.svg`
rect {
fill: steelblue;
shape-rendering: crispEdges;
}
text {
fill: black;
font: 10px sans-serif;
text-anchor: middle;
font-weight: bold;
}
.start, .note {
text-anchor: start;
}
`
let height = 150;
let width = 700;
const maxDepthVal = 35;
class Alignment extends React.Component{
constructor(props){
super(props);
if(props.height){
height = props.height;
}
if(props.width){
width = props.width;
}
}
findMax = (data, key) => {
return Math.max.apply(Math, data.map(o => o[key]))
}
renderAlignmentComponent = (arr, y, ref, barWidth, leftPadding) => {
d3.select(ref).selectAll('text')
.data(arr, d => d.key)
.join(
enter => enter.append('text')
.attr('transform', (d, i) => `translate(${leftPadding + (i * (barWidth)) }, 0)`)
.attr('x', (d, i) => barWidth/2)
.attr('y', y)
.text(d => d.base),
update => update
.attr('transform', (d, i) => `translate(${leftPadding + i * barWidth}, 0)`)
.attr('x', (d, i) => barWidth/2)
)
}
renderMarker = (data, ref, y ) => {
d3.select(ref).selectAll('text').data(data)
.join('text')
.attr('x', 10)
.attr('y', y)
.attr('class', 'start')
.attr('dy', '.75em')
.text(d => d)
}
renderLabel = (data, ref, y) => {
d3.select(ref)
.selectAll('text')
.data(data)
.join(
enter => enter.append('text')
.attr('x', 10)
.attr('y', y)
.attr('class', 'note')
.attr('dy', '.75em')
.text(d => d),
update => update,
exit => exit.remove()
)
}
componentDidUpdate = () => {
const { reference, alignment, depth, start } = this.props;
const leftPadding = 40 + start.toString().length * 4.5;
const topPadding = reference.length > 0 ? 38 : 0;
const barWidth = (width - leftPadding) / depth.length;
let i;
const chart = d3.select(this.refs.chart)
.attr('width', width)
.attr('height', height + topPadding + 5)
const unencodedDepths = []
const alArr = [];
const refArr = [];
const seps = [];
let max = 0;
for(i = 0; i < depth.length; i+=1){
const key = i + start;
if(reference.length > 0){
alArr.push({
base: alignment[i],
key
})
refArr.push({
base: reference[i],
key
})
seps.push({
base: reference[i] === alignment[i] ? '|' : '',
key
});
}
let depthVal;
if(depth[i].match(/[a-z]/i)){
depthVal = depth[i].charCodeAt(0) - 87; //so that a = 10
}else{
depthVal = +depth[i];
}
max = max > depthVal ? max : depthVal;
unencodedDepths.push({
depth: depthVal,
key
});
}
const y = d3.scaleLinear()
.range([height, 0])
.domain([0, max]);
//start pos marker
this.renderMarker([start], this.refs.startPos, topPadding + 7);
this.renderMarker([max], this.refs.height, height + topPadding - 10);
if(reference.length > 0){
this.renderAlignmentComponent(refArr, 10, this.refs.reference, barWidth, leftPadding);
this.renderAlignmentComponent(alArr, 38, this.refs.alignEl, barWidth, leftPadding);
this.renderAlignmentComponent(seps, 23, this.refs.sepEl, barWidth, leftPadding);
}
//histogram
d3.select(this.refs.hist).selectAll('rect')
.attr('height', height)
.data(unencodedDepths, d => d.key)
.join(
enter => enter.append('rect')
.attr('transform', (d, i) => `translate(${leftPadding + (i * barWidth) - barWidth/2}, 0)`)
.attr('y', topPadding + 5)
.attr('x', barWidth/2)
.attr('height', d => height - y(d.depth))
.attr('width', barWidth),
update => update
.attr('transform', (d, i) => `translate(${leftPadding + (i * barWidth) - barWidth/2}, 0)`)
.attr('height', d => height - y(d.depth))
.attr('x', (d, i) => barWidth/2)
)
this.renderLabel(reference.length > 0 ? ['Sbject'] : '', this.refs.refNote, 2);
this.renderLabel(reference.length > 0 ? ['Query'] : '', this.refs.alNote, 30);
}
componentDidMount = () => {
this.componentDidUpdate();
}
render(){
return (
<StyledSVG ref="chart">
<StyledSVG ref="refNote" />
<StyledSVG ref="alNote" />
<StyledSVG ref="startPos" />
<StyledSVG ref="height" />
<StyledSVG ref="reference" />
<StyledSVG ref="sepEl" />
<StyledSVG ref="alignEl" />
<StyledSVG ref="hist" />
</StyledSVG >
)
}
}
Alignment.propTypes = {
reference: PropTypes.string.isRequired,
alignment: PropTypes.string.isRequired,
start: PropTypes.number.isRequired,
depth: PropTypes.string.isRequired
}
export default Alignment;
The code works well, and re-renders correctly when I change the props to show the next bit of the alignment.
To me, however, it seems excessive with the d3 select calls - but i'm not sure if there's a better way here.
Any suggestions are appreciated!
Toby Speight
87k14 gold badges104 silver badges322 bronze badges
default