--- a +++ b/singlecellmultiomics/utils/copyNumberStatePlotter.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from singlecellmultiomics.utils import bdbplot,organoidTools +from copy import deepcopy +from importlib import reload +import numpy as np + +class StatePlotter(): + + def __init__(self, plot=None): + + + self.canvas = bdbplot.BDBPlot() if plot is None else plot + self.heightPerState = 10 + self.stateMargin = 1 + self.chromosomeMargin = 2 + self.pixelsPerBase = 1/10_000_000 + self.headerHeight=30 + self.bigFontSize = 10 + self.smallFontSize = 6 + self.gray = 220 + + self.gainColor = '#B36466' + self.gainTwoColor = '#FF0087' + self.normalColor = '#FFFFFF' + self.lossColor = '#6562B5' + self.totalLossColor = '#004BD8' + self.missingColor = '#AAAAAA' + + def plotStates(self, df, offset=(0,0), **kwargs): + self.offset = offset + stateGroup = self.canvas.getGroup('stateGrid') + self.stateGroup = stateGroup + stateGroup.set('transform' ,f"translate({offset[0]},{offset[1]})" ) + self.canvas.svgTree.append(stateGroup) + self.states = sorted(list(set(df['cluster']))) + self.chromosomeOrder = sorted(sorted(list(set(df[ 'chromosome']))), key=organoidTools.chrom_sort_human) + + x = 10 + + for i,state in enumerate(self.states): + g = self.canvas.getGroup(f'state_{state}') + stateGroup.append(g) + + result = self.plotState(self.canvas, g, df[ df['cluster']==state ], x, + y= (self.heightPerState*i + (i*self.stateMargin)), label=state , + plotChromosomeLabels=(i==0), **kwargs + ) + + return self.canvas + + + def plotState(self, plot, g, row, x, y, label, plotChromosomeLabels=False, + logScale=False, chromosomeSizes=None, logRepeats=False): + + currentX = x + + + shownAlleles = False + for chromosome in self.chromosomeOrder: + print(chromosome) + isAllelic = chromosome.endswith('_A') or chromosome.endswith('_B') + #Obtain how many bases this chromosome has + chrom = chromosome.split('_')[0] + if chromosomeSizes is not None and chrom in chromosomeSizes: + chromosomeSize = chromosomeSizes[chrom] + else: + + chromosomeSize = row[ row['chromosome']==chromosome ]['endCoordinate'].max() + + + chromosomePixelWidth = chromosomeSize * self.pixelsPerBase + + rect = self.canvas.getRectangle(currentX, y, chromosomePixelWidth, self.heightPerState) + self.canvas.modifyStyle( rect, {'fill':f'rgb({self.gray},{self.gray},{self.gray})', 'stroke':'none'}) + g.append(rect) + + if plotChromosomeLabels: + + if not chromosome.endswith('_B'): # allelic + text = plot.getText(chromosome.replace('chr','').replace('_A',''), currentX+chromosomePixelWidth*0.5 + (chromosomePixelWidth*0.5 if chromosome.endswith('_A') else 0), y - self.heightPerState) + text.set('text-anchor','middle') + text.set('dominant-baseline','middle') + text.set('font-family','Helvetica') + text.set('font-size', str(self.smallFontSize)) + + g.append(text) + + if isAllelic: + alleleDescriptor = 'allele ' if not shownAlleles else '' + allele = 'A' if chromosome.endswith('_A') else 'B' + text = self.canvas.getText( f'{alleleDescriptor}{allele}', currentX+chromosomePixelWidth*0.5 , y - 0.4*self.heightPerState) + text.set('text-anchor','middle') + text.set('dominant-baseline','middle') + text.set('font-family','Helvetica') + text.set('font-size', str(self.smallFontSize*0.8)) + g.append(text) + if allele=='B': + shownAlleles=True + + if isAllelic: + offset = self.chromosomeMargin*0.8 + r = self.canvas.getRectangle(currentX-offset, y-self.headerHeight*0.5, + chromosomePixelWidth+offset*2, self.heightPerState+offset*2+self.headerHeight*0.5) + r.set('z-index', '0') + self.canvas.modifyStyle( r, {'fill':'#DCFFCE', 'stroke':'none'}) + self.stateGroup.insert(0,r) + + + withinX = currentX + + binSizes = {} + if logScale: + currCoord = 0 + minBinSize = 1_000 + + for binIndex in sorted(list(row[ row['chromosome']==chromosome ]['binIndex'])): + + d = row[ row['chromosome']==chromosome ]['binIndex']==binIndex + dat = row[ row['chromosome']==chromosome ][d].iloc[0,:] + cn =dat['copyNumber'] + + space = dat['startCoordinate'] - currCoord + if space > minBinSize: # add intermediate bin: + binSizes[(binIndex, 'spacer')] = np.log(space)/10 if logRepeats else space/chromosomePixelWidth + + + size = np.log((dat['endCoordinate'] - dat['startCoordinate'] )) + binSizes[binIndex] = size + currCoord=dat['endCoordinate'] + + space = chromosomeSize - currCoord + if space > minBinSize: # add intermediate bin: + binSizes[(binIndex, 'spacerfinal')] = np.log(space)/10 if logRepeats else space/chromosomePixelWidth + + + for binIndex in sorted(list(row[ row['chromosome']==chromosome ]['binIndex'])): + d = row[ row['chromosome']==chromosome ]['binIndex']==binIndex + dat = row[ row['chromosome']==chromosome ][d].iloc[0,:] + cn =dat['copyNumber'] + + if logScale: + if (binIndex, 'spacer') in binSizes: + withinX+=( binSizes[(binIndex,'spacer')] / sum( binSizes.values() ) ) * chromosomePixelWidth + + size = ( binSizes[binIndex] / sum( binSizes.values() ) ) * chromosomePixelWidth + + + else: + size = (dat['endCoordinate'] - dat['startCoordinate'] )*self.pixelsPerBase + + r = self.canvas.getRectangle(withinX, y, size, self.heightPerState) + self.canvas.modifyStyle(r, {'stroke-width':'0.2'}) + fillAttr='fill' + + if chromosome.endswith('_A') or chromosome.endswith('_B'): # allelic + cn+=1 # (diploid color == white == 2 ) + + + if cn >= 4: + self.canvas.modifyStyle(r, {fillAttr:self.gainTwoColor}) + if cn == 3: + self.canvas.modifyStyle(r, {fillAttr:self.gainColor}) + if cn==2: + self.canvas.modifyStyle(r, {fillAttr:self.normalColor}) + if cn==1: + self.canvas.modifyStyle(r, {fillAttr:self.lossColor}) + if cn==0: + self.canvas.modifyStyle(r, {fillAttr:self.totalLossColor}) + if np.isnan(cn): + self.canvas.modifyStyle(r, {fillAttr:self.missingColor}) + + g.append(r) + + withinX+=size + + currentX += chromosomePixelWidth+self.chromosomeMargin + + ##### plot the label: + text = self.canvas.getText(label, currentX, y+0.5*self.heightPerState) + text.set('text-anchor','begin') + text.set('dominant-baseline','middle') + text.set('font-family','Helvetica') + text.set('font-size', str(self.bigFontSize)) + + + g.append(text) + + self.canvas.setWidth( max(self.canvas.width, currentX+self.chromosomeMargin+20+ self.offset[0] ) ) + self.canvas.setHeight( max(self.canvas.height, y+self.heightPerState*1.5+ self.offset[1] )) + + return {'x':x}