Differences in Mutational Tolerance Between RBP and F

Relative frequencies of mutation effects for RBP and F proteins

We compared the distribution of mutation effects for the Nipah virus receptor binding protein (RBP) and fusion protein (F). RBP is more tolerant of mutations, with a higher fraction of mutations having near-neutral effects on function, while F is less tolerant of mutations, with a higher fraction of mutations being deleterious. This difference in mutational tolerance likely reflects the different functional constraints on the two proteins, with F needing to maintain a highly specific conformation to mediate membrane fusion, while RBP has more flexibility in its structure to accommodate binding to different host receptors. F data are from the current project, while RBP data are from our previous deep mutational scan of Nipah RBP.

Code

vue
<template>
    <svg ref="svgContainer"></svg>
</template>

<script setup>
import { shallowRef } from 'vue';
import * as d3 from 'd3';
import { withBase } from 'vitepress';

// DEFINE VARIABLES
const svgContainer = shallowRef(null);
const dataPath = withBase('/data/RBP_vs_F_func_effects.csv');

// Set dimensions and margins
const width = 400;
const height = 400;
const marginTop = 20;
const marginRight = 30;
const marginBottom = 60;
const marginLeft = 60;
const colorScale = d3.scaleOrdinal(d3.schemeCategory10)

const bandwidth = 0.4; // Adjust this value to change the smoothness of the density curves

const fetchData = async () => {
    try {
        const array = await d3.csv(dataPath, (d) => ({
            site: +d.site,
            wildtype: d.wildtype,
            mutant: d.mutant,
            effect: +d.effect,
            protein: d.protein,
        }));

        createDensityPlot(array)
    } catch (error) {
        console.error('Error loading CSV data:', error)
    }
}
fetchData()

// Create the density plot
const createDensityPlot = (array) => {


    // Get unique proteins and create color scale
    const proteins = [...new Set(array.map(d => d.protein))]


    // Create scales
    const xExtent = d3.extent(array, d => d.effect)
    const xScale = d3.scaleLinear()
        .domain([xExtent[0] - 0.25, xExtent[1] + 0.25])
        .range([marginLeft, width - marginRight])


    const yScale = d3.scaleLinear()
        .domain([0, 1])
        .range([height - marginBottom, marginTop])


    // Calculate density for each protein
    const densities = proteins.map(protein => {
        const proteinData = array
            .filter(d => d.protein === protein)
            .map(d => d.effect);
        const thresholds = d3.ticks(...d3.nice(...d3.extent(proteinData), 10), 40);
        return {
            protein: protein,
            density: kde(epanechnikov(bandwidth), thresholds, proteinData)
        }
    })

    // Create line generator
    const line = d3.line()
        .x(d => xScale(d[0]))
        .y(d => yScale(d[1]))
        .curve(d3.curveBasis)

    // Create SVG
    const svg = d3.select(svgContainer.value)
        .attr('viewBox', `0 0 ${width} ${height}`)

    // Draw density curves
    densities.forEach(d => {
        svg.append('path')
            .datum(d.density)
            .attr('fill', colorScale(d.protein))
            .attr('stroke', colorScale(d.protein))
            .attr('stroke-width', 2)
            .attr('fill-opacity', 0.3)
            .attr('d', line)
    })

    // x axis
    const xAxis = svg.append('g')
        .attr('transform', `translate(0,${height - marginBottom})`)
        .call(d3.axisBottom(xScale).ticks(4).tickSizeOuter(0))

    xAxis.selectAll('text')
        .attr('fill', 'currentColor')
        .attr('font-size', '12px')

    xAxis.append('text')
        .attr('class', 'label')
        .attr('x', width / 2)
        .attr('y', marginBottom - 10)
        .attr('text-anchor', 'middle')
        .attr('fill', 'currentColor')
        .style('font-size', '14px')
        .style('font-weight', 'bold')
        .text('Effects of mutations on cell entry in CHO-bEFNB3 cells');

    // y axis
    const yAxis = svg.append('g')
        .attr('transform', `translate(${marginLeft},0)`)
        .call(d3.axisLeft(yScale).ticks(4).tickSizeOuter(0))

    yAxis.selectAll('text')
        .attr('fill', 'currentColor')
        .attr('font-size', '12px')

    yAxis.append('text')
        .attr('class', 'label')
        .attr('x', -height / 3.5)
        .attr('y', -marginLeft + 15)
        .attr('transform', 'rotate(-90)')
        .attr('fill', 'currentColor')
        .style('font-size', '14px')
        .style('font-weight', 'bold')
        .text('Relative Frequency');

    // legend
    const legend = svg.append('g')
        .attr('transform', `translate(${width - 80}, ${marginTop})`)

    proteins.forEach((protein, i) => {
        const legendRow = legend.append('g')
            .attr('transform', `translate(0, ${i * 20})`)

        legendRow.append('rect')
            .attr('width', 18)
            .attr('height', 2)
            .attr('fill', colorScale(protein))

        legendRow.append('text')
            .attr('x', 25)
            .attr('y', 5)
            .style('font-size', '12px')
            .attr('fill', 'currentColor')
            .text(protein)
    })
}

// Kernel density estimation functions
function kde(kernel, thresholds, data) {
    return thresholds.map(t => [t, d3.mean(data, d => kernel(t - d))]);
}

function epanechnikov(bandwidth) {
    return function (v) {
        return Math.abs(v /= bandwidth) <= 1 ? 0.75 * (1 - v * v) / bandwidth : 0;
    };
}
</script>