Skip to content

Commit

Permalink
fix: Variation Landscape downsampling
Browse files Browse the repository at this point in the history
  • Loading branch information
gromdimon committed Feb 9, 2024
1 parent 871c801 commit 6a50f9d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 47 deletions.
73 changes: 26 additions & 47 deletions src/components/GeneClinvarCard/VariationLandscapePlotly.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ClinicalSignificance } from '../../pbs/annonars/clinvar/minimal'
import { Record as ClinvarRecord } from '../../pbs/annonars/clinvar/minimal'
import { ClinvarPerGeneRecord } from '../../pbs/annonars/clinvar/per_gene'
import { Transcript } from '../../pbs/mehari/txs'
import { type PlotlyDataPoint, downsample } from './lib'
/** This component's props. */
const props = withDefaults(
Expand All @@ -31,6 +32,13 @@ const props = withDefaults(
}
)
/** The current plot boundaries. */
const currentPlotBoundaries = ref({
minX: 0,
maxX: 0
})
/* Enumeration clinical significance. */
const CLINVAR_SIGNIFICANCE_TO_INT: { [Key in ClinicalSignificance]: number } = {
[ClinicalSignificance.CLINICAL_SIGNIFICANCE_UNKNOWN]: -3,
[ClinicalSignificance.CLINICAL_SIGNIFICANCE_PATHOGENIC]: 2,
Expand All @@ -40,15 +48,15 @@ const CLINVAR_SIGNIFICANCE_TO_INT: { [Key in ClinicalSignificance]: number } = {
[ClinicalSignificance.CLINICAL_SIGNIFICANCE_BENIGN]: -2
}
/* Convert clinical significance to integer. */
const convertClinvarSignificance = (input: ClinicalSignificance): number => {
if (input in CLINVAR_SIGNIFICANCE_TO_INT) {
return CLINVAR_SIGNIFICANCE_TO_INT[input]
} else {
return -4
}
}
/* Compute color for each point */
/* Helper function to compute color for point value. */
const markerColor = (value: number) => {
if (value === 2) {
return 'red'
Expand All @@ -65,23 +73,6 @@ const markerColor = (value: number) => {
}
}
/** Interface for plotly data point. */
interface PlotlyDataPoint {
x: number
y: number
}
/** Interface for downsampled data point. */
interface DownsampledDataPoint {
x: number
y: number
count: number
}
/** The current plot boundaries. */
const currentPlotBoundaries = ref({
minX: 0,
maxX: 0
})
/** Compute the min and max values for the plot */
const clinvarData = computed<PlotlyDataPoint[]>(() => {
if (!props.clinvarPerGene) {
Expand All @@ -105,42 +96,23 @@ const clinvarData = computed<PlotlyDataPoint[]>(() => {
}))
})
/** Helper function for downstreaming data.
* @param data - The data to downsample.
* @param windowSize - The size of the window to downsample.
* @returns The downsampled data.
*/
const downsample = (data: PlotlyDataPoint[], windowSize: number): DownsampledDataPoint[] => {
if (data.length === 0) return []
// If there are less then 800 variants, do not downsample
if (data.length < 800) return data.map((item) => ({ x: item.x, y: item.y, count: 1 }))
const minX = data[0].x
const maxX = data[data.length - 1].x
const bins: DownsampledDataPoint[] = []
for (let x = minX; x <= maxX; x += windowSize) {
for (let y = -3; y <= 2; y++) {
bins.push({ x: x + windowSize / 2, y: y, count: 0 })
}
}
data.forEach((point) => {
const binIndex = Math.floor((point.x - minX) / windowSize)
const bin = bins[binIndex * 5 + (point.y + 3)]
bin.count++
})
// Return only bins with count > 0
return bins.filter((bin) => bin.count > 0)
}
/** Downsampled data. */
const plotlyData = computed(() => {
// If there are less then 800 variants, return the data
// If there are less then 800 variants, return the data TODO
if (clinvarData.value.length < 800) {
return clinvarData.value
}
const windowSize = (currentPlotBoundaries.value.maxX - currentPlotBoundaries.value.minX) / 800
return downsample(clinvarData.value, windowSize)
return downsample(
clinvarData.value,
windowSize,
currentPlotBoundaries.value.minX,
currentPlotBoundaries.value.maxX
)
// DEBUG: Uncomment the line below to return the full data and compare with the original plot
// return clinvarData.value
})
/** Plotly trace */
const trace = {
uid: 'fc47f27b-f3b0-4d31-8dac-9782780ba6b8',
Expand All @@ -156,6 +128,7 @@ const trace = {
},
scaleanchor: 'y'
}
/** Initial Lollipop sticks for each variant */
const initiallollipopSticks = computed(() => {
if (!props.clinvarPerGene) {
Expand All @@ -179,6 +152,7 @@ const initiallollipopSticks = computed(() => {
}
return sticks
})
/** Exons for the gene */
const exons = computed(() => {
if (!props.transcripts) {
Expand All @@ -197,6 +171,7 @@ const exons = computed(() => {
}
return exons
})
// Horizontal line at y=3
const horizontalLine = {
type: 'line',
Expand All @@ -211,6 +186,7 @@ const horizontalLine = {
width: 2
}
}
// Grey rectangles for exons
const exonShapes = exons.value.map((exon) => ({
type: 'rect',
Expand All @@ -225,6 +201,7 @@ const exonShapes = exons.value.map((exon) => ({
width: 0
}
}))
/** Plot layout */
const layout = {
title: 'Variation Landscape',
Expand Down Expand Up @@ -258,12 +235,13 @@ const layout = {
pad: 4
}
}
/** Method to update plot with new data based on zoom or pan */
const updatePlotData = (minX: number, maxX: number) => {
// Filter clinvarData for new boundaries and compute new downsampled data
const windowSize = (maxX - minX) / 800
const filteredClinvarData = clinvarData.value.filter((item) => item.x >= minX && item.x <= maxX)
const newDownsampledData = downsample(filteredClinvarData, windowSize)
const newDownsampledData = downsample(filteredClinvarData, windowSize, minX, maxX)
trace.x = newDownsampledData.map((item) => item.x)
trace.y = newDownsampledData.map((item) => item.y)
trace.marker.color = newDownsampledData.map((item) => markerColor(item.y))
Expand All @@ -288,6 +266,7 @@ const updatePlotData = (minX: number, maxX: number) => {
// Re-render plot with new data
Plotly.react('plot', [trace], layout)
}
onMounted(() => {
Plotly.newPlot('plot', [trace], layout, { scrollZoom: true }).then(() => {
// @ts-expect-error Property 'on' seems to exist on type 'HTMLElement'
Expand Down
Empty file.
56 changes: 56 additions & 0 deletions src/components/GeneClinvarCard/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/** Interface for plotly data point. */
export interface PlotlyDataPoint {
x: number
y: number
}

/** Interface for downsampled data point. */
export interface DownsampledDataPoint {
x: number
y: number
count: number
}

/** Helper function for downstreaming data.
* @param data - The data to downsample.
* @param windowSize - The size of the window to downsample.
* @param start - The start of the data.
* @param end - The end of the data.
* @returns The downsampled data.
*/
export const downsample = (
data: PlotlyDataPoint[],
windowSize: number,
start: number,
end: number
): DownsampledDataPoint[] => {
// First, filter the data points to only include those within the start and end range
const filteredData = data.filter((item) => item.x >= start && item.x <= end)

if (filteredData.length === 0) return []

// If there are less than 800 variants after filtering, do not downsample
if (filteredData.length < 800) {
return filteredData.map((item) => ({ x: item.x, y: item.y, count: 1 }))
}

const bins: DownsampledDataPoint[] = []
const minX = start // Use the provided start as minX
const maxX = end // Use the provided end as maxX

for (let x = minX; x <= maxX; x += windowSize) {
for (let y = -3; y <= 2; y++) {
bins.push({ x: x + windowSize / 2, y: y, count: 0 })
}
}

filteredData.forEach((point) => {
const binIndex = Math.floor((point.x - minX) / windowSize)
if (binIndex < 0 || binIndex >= bins.length / 6) return // Guard against out-of-range errors
const bin = bins[binIndex * 6 + (point.y + 3)]
bin.count++
})

// Return only bins with count > 0
return bins.filter((bin) => bin.count > 0)
}

0 comments on commit 6a50f9d

Please sign in to comment.