5
\$\begingroup\$

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:

enter image description here

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
asked Nov 21, 2019 at 8:29
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.