Animation of Cell Entry by Amino Acid Letters
Unmutated residue and site number are placed near the top, with increasing deleterious effects as you go down
Code
vue
<template>
<svg ref="svgContainer"></svg>
</template>
<script setup>
import { ref, onMounted, onUnmounted, shallowRef} from 'vue';
import * as d3 from 'd3';
import { withBase } from 'vitepress';
// REACTIVE VARIABLES
const svgContainer = shallowRef(null);
// D3 CONFIGURATION
const width = 600;
const height = 300;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 20;
const marginLeft = 20;
const innerWidth = width - marginLeft - marginRight;
const innerHeight = height - marginTop - marginBottom;
const aminoAcids = ['R', 'K', 'H', 'D', 'E', 'Q', 'N', 'S', 'T', 'Y', 'W', 'F', 'A', 'I', 'L', 'M', 'V', 'G', 'P', 'C'];
const t = 2000; // Transition duration
const t_type = d3.easePoly; // Transition type
const animationSpeed = 2500; // Time between animations
// LOAD DATA
const dataFile = withBase('/data/Nipah_F_func_effects_filtered.csv');
let data = [];
let g = null;
let currentSite = 29;
const fetchData = async () => {
try {
const rawData = await d3.csv(dataFile);
const dataBysite = new Map();
rawData.forEach(d => {
const site = +d.site;
const record = {
site: site,
wildtype: d.wildtype,
mutant: d.mutant,
effect: parseFloat(d.effect),
};
if (!dataBysite.has(site)) {
dataBysite.set(site, []);
}
dataBysite.get(site).push(record);
});
return dataBysite;
} catch (error) {
console.error('Error loading CSV data:', error);
}
};
// Initialize visualization
onMounted(async () => {
data = await fetchData();
const svg = d3.select(svgContainer.value)
.attr('viewBox', `0 0 ${width} ${height}`)
.append('g')
g = svg.append('g')
.attr('transform', `translate(${marginLeft}, ${marginTop})`);
const siteData = data.get(currentSite);
updateVisualization(siteData);
// Start animation
d3.interval(() => {
currentSite = currentSite + 1;
const newSiteData = data.get(currentSite);
updateVisualization(newSiteData);
}, animationSpeed);
});
const x = d3.scaleBand()
.domain(aminoAcids)
.range([0, innerWidth])
.padding(0.1);
const y = d3.scaleLinear()
.domain([-3.75, 1])
.range([innerHeight, 0]);
const colorScale = d3.scaleDiverging()
.domain([-3.5, 0, 0.1]) // [positive, neutral, negative]
.interpolator(d3.interpolateRdBu)
// Update visualization when data or site changes
const updateVisualization = (siteData) => {
// Update mutants
const mutant = g.selectAll('.mutant-letter')
.data(siteData, d => d.mutant);
// Remove old mutant
mutant.exit()
.transition()
.ease(t_type)
.duration(t/2)
.attr('opacity', 0)
.remove();
// Add new mutants
const mutantsEnter = mutant.enter()
.append('text')
.attr('class', 'mutant-letter')
.attr('x', d => x(d.mutant))
.attr('opacity', 0)
.attr('y', d => d.effect >= 0 ? y(d.effect) : y(0))
.style('fill', d => colorScale(d.effect))
.attr('font-size', '24px')
.attr('font-weight', 'bold')
.attr('text-anchor', 'middle')
.attr('text-align', 'center')
.text(d => d.mutant)
// Update all mutants
mutant.merge(mutantsEnter)
.transition()
.ease(t_type)
.duration(t)
.attr('x', d => x(d.mutant))
.attr('opacity', 1)
.attr('y', d => Math.abs(y(d.effect) - y(1)))
.style('fill', d => colorScale(d.effect))
.text(d => d.mutant);
// Update wildtype
const currentWildtype = siteData[0].wildtype;
const wildtypeData = [currentWildtype];
const wildtype = g.selectAll('.wildtype-letter')
.data(wildtypeData, d => d);
// Remove old wildtype
wildtype.exit()
.transition()
.ease(t_type)
.duration(t/3)
.attr('opacity', 0)
.remove();
// Add new wildtypes
const wildtypeEnter = wildtype.enter()
.append('text')
.attr('class', 'wildtype-letter')
.attr('x', x(currentWildtype))
.attr('opacity', 0.2)
.attr('y', y(0))
.style('fill', 'currentColor')
.attr('text-anchor', 'middle')
.attr('text-align', 'center')
.attr('font-size', '24px')
.attr('font-weight', 'bold')
.text(d => d)
// Update all wildtypes
wildtype.merge(wildtypeEnter)
.transition()
.ease(t_type)
.duration(t)
.attr('x', x(currentWildtype))
.attr('opacity', 1)
.attr('font-size', '24px')
.style('fill', 'currentColor')
.text(d => d);
const siteText = g.selectAll('.site-text')
.data([siteData[0]], d => d.wildtype + d.site);
siteText.exit().transition()
.ease(t_type)
.duration(t/3)
.attr('opacity', 0)
.remove();
const siteTextEnter = siteText.enter()
.append('text')
.attr('class', 'site-text')
.attr('x', d => x(d.wildtype))
.attr('y', y(0.5))
.attr('text-anchor', 'middle')
.attr('text-align', 'center')
.attr('font-size', '16px')
.attr('fill', 'currentColor')
.attr('opacity', 0)
.merge(siteText)
.text(d => `${d.site}`);
siteText.merge(siteTextEnter)
.transition()
.ease(t_type)
.duration(t/5)
.attr('x', d => x(d.wildtype))
.attr('opacity', 1)
.text(d => `${d.site}`);
};
onUnmounted(() => {
// Remove all transitions to prevent memory leaks
if (g) {
g.selectAll('*').interrupt();
}
});
</script>
<style scoped>
</style>