Switch to side-by-side view

--- 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}