Protein is Good for You, part 2 (Matt Gertz)
In yesterday’s blog post, I walked through an engine for translating DNA to its amino acid results via messenger RNA. In today’s blog, we’ll work on the visualization using WPF StackPanels. (This example requires VS2008, although it could certainly be written in WinForms as well with just a little bit of extra work.)
Caution: The point of this blog is to demonstrate StackPanels, and specifically how you can control their orientation and nesting. Consequently, for the sake of that argument, I’m creating a lot of text objects (one for each base and each amino) and nesting them to demonstrate this. For a really robust program, I would ditch the extra objects and just do owner draw directly. In other words, don’t use this example on enormous 20000-sequence strands of DNA, as neither you nor your PC would like the performance results.
StackPanels
In the last post, I added a ScrollViewer to the Window, changed it to scroll horizontally instead of vertically, but otherwise left it alone. Now we’ll dig into the details of how we’ll populate it with StackPanels.
StackPanels expose a very easy way to organize a set of drawable objects on the screen. StackPanels expose a Children collection, and when you add objects to the that collection, they’ll display in the order you add them and in the direction you specify. What’s really cool is that StackPanels will nest with each other, and I’ll take advantage of that fact by creating three StackPanels that organize information horizontally (one each for DNA, mRNA, and aminos), and then adding them to a parent StackPanel so that they line up with each other. I’ll then add the parent StackPanel to the ScrollViewer so that the alignment of all three sub-panels can be seen.
Each of the sub-panels will contain one of more text objects which either contain the abbreviation of a base, the abbreviation of an amino, or blank space (representing unused intron material). Aminos correspond to three bases, and so those text objects need to be three times wider. I’ll create some helper functions to return the appropriate text objects:
Const baseWidth As Integer = 16
Private Function DrawBase(ByVal base As Char) As TextBlock
Dim baseTextBlock As New TextBlock
Select Case base ' Switch for colors
Case "C"
baseTextBlock.Background = Brushes.LightSalmon
Case "G"
baseTextBlock.Background = Brushes.LightGoldenrodYellow
Case "A"
baseTextBlock.Background = Brushes.LightGreen
Case "T"
baseTextBlock.Background = Brushes.LightSkyBlue
Case "U"
baseTextBlock.Background = Brushes.LightSteelBlue
End Select
baseTextBlock.Inlines.Add(New Bold(New Run(base)))
baseTextBlock.Height = 32
baseTextBlock.Width = baseWidth
baseTextBlock.TextAlignment = TextAlignment.Center
Return baseTextBlock
End Function
That last function will be used by both DNA and RNA, creating and returning a fixed block of an appropriate color (a different one for each base) 16 x 32 pixels with the base abbreviation (passed into the function) centered within it. I've chosen light colors so that the dark text will show up against the background properly.
For the amino chains, it’s pretty similar, except that each block is three times wider, to keep the amino lined up with the codon:
Private Function DrawAmino(ByVal amino As String) As TextBlock
Dim aminoTextBlock As New TextBlock
Select Case amino ' Switch for colors
Case "Phe"
aminoTextBlock.Background = Brushes.AliceBlue
Case "Leu"
aminoTextBlock.Background = Brushes.Beige
Case "Ser"
aminoTextBlock.Background = Brushes.Cyan
Case "Tyr"
aminoTextBlock.Background = Brushes.LightSeaGreen
' (Etc… the other aminos omitted for brevity’s sake; see final code for full list.)
End Select
aminoTextBlock.Inlines.Add(New Bold(New Run(amino)))
aminoTextBlock.Height = 32
aminoTextBlock.Width = baseWidth * 3
aminoTextBlock.TextAlignment = TextAlignment.Center
Return aminoTextBlock
End Function
And for empty space, I just pass in the number of spaces required and multiple it by the baseWidth:
Private Function DrawNoMap(ByVal noMapSize As Integer) As TextBlock
Dim noMapTextBlock As New TextBlock
noMapTextBlock.Background = ScrollViewer1.Background
noMapTextBlock.Height = 32
noMapTextBlock.Width = baseWidth * noMapSize
Return noMapTextBlock
End Function
Now I have the tools; we can put them to good use. First, let’s create & populate the DNA StackPanel:
Private Function DrawBases(ByVal s As String) As StackPanel
' DNA goes here (horizontal orientation):
Dim myStackPanel As New StackPanel
myStackPanel.HorizontalAlignment = System.Windows.HorizontalAlignment.Left
myStackPanel.VerticalAlignment = System.Windows.VerticalAlignment.Top
myStackPanel.Orientation = Orientation.Horizontal
'Add child elements to the parent StackPanel.
For i As Integer = 0 To s.Length - 1
myStackPanel.Children.Add(DrawBase(s(i)))
Next
Return myStackPanel
End Function
This is a simple function that does three things:
(1) Create a stack panel that has horizontal orientation, containing object that start from the top left.
(2) Step through each base in the DNA or RNA and add the TextBlock for it created in DrawBase.
(3) Return the completed StackPanel
I can use this for both DNA and mRNA just by passing in the appropriate string. Aminos are a bit trickier because I’ve decided to save them as lists of strings rather than one long string (to make it easier to access individual proteins should I wish to expand this program in the future):
Private Function DrawAminos() As StackPanel
' Aminos go here (horizontal orientation):
Dim myStackPanelAminos As New StackPanel
myStackPanelAminos.HorizontalAlignment = System.Windows.HorizontalAlignment.Left
myStackPanelAminos.VerticalAlignment = System.Windows.VerticalAlignment.Top
myStackPanelAminos.Orientation = Orientation.Horizontal
'Add child elements to the parent StackPanel.
For Each p In Proteins
Dim i As Integer = 0
Do While i < p.Length - 1
If p(i) = " " Then
Dim noMapSize As Integer = 0
Do While i < p.Length AndAlso p(i) = " " ' Make sure we don't go over when lining things up
noMapSize += 1
i += 1 ' Skip piece of intron for spacing
Loop
myStackPanelAminos.Children.Add(DrawNoMap(noMapSize))
Else
myStackPanelAminos.Children.Add(DrawAmino(p.Substring(i, 3)))
i += 3 ' Skip length of amino
End If
Loop
Next
Return myStackPanelAminos
End Function
In this case, the panel is once again oriented horizontally from the top-left, but I have to (a) go through each string in the list looking for aminos to draw and (b) determine if there are spaces that need to take up room in the drawing but don’t contain actual amino content. Sequences without a stop codon would always be the final sequence (because the engine would have run through the whole string looking for them) and are just ignored by the application; they imply space without having to explicitly draw that space, since nothing else comes after it.
I don’t want to create a text block for each space in an adjacent block of spaces – that’s too wasteful even for a simple example like this – so I’ll determine the number of adjacent spaces whenever I encounter them and group them together in on big “no map” block. The “gotcha” in the above code is that I need to make sure that I don’t go past the final character both when looking for aminos as well as when drilling through the spaces.
Now, having created all of the drawing code as well as the code that runs through the sequences and calls them, I can create a master function that kicks off the drawing. First, I need to get rid of any old content in the ScrollViewer, tossing it into the garbage collector:
Private Sub DrawResults()
'Get rid of any old content
Me.ScrollViewer1.Content = Nothing
Next, I’ll create the master stack panel. Unlike the others, its orientation is vertical (which is the default, so I don’t need to explicitly state it), because I’m going to stack the other panels vertically from top-left:
' This stack panel will contian the other stack panels.
' It defaults to a vertical orientation
Dim myStackPanel As New StackPanel
myStackPanel.HorizontalAlignment = System.Windows.HorizontalAlignment.Left
myStackPanel.VerticalAlignment = System.Windows.VerticalAlignment.Top
Now, I’ll use the previously defined helper functions to create and draw the sub-panels, and then add them to the parent StackPanel:
Dim myStackPanelDNA As StackPanel = DrawBases(DNA)
Dim myStackPanelRNA As StackPanel = DrawBases(RNA)
Dim myStackPanelAminos As StackPanel = DrawAminos()
myStackPanel.Children.Add(myStackPanelDNA)
myStackPanel.Children.Add(myStackPanelRNA)
myStackPanel.Children.Add(myStackPanelAminos)
Finally, I add the parent panel to the ScrollViewer:
'Add the StackPanel as the lone Child of the Border
Me.ScrollViewer1.Content = myStackPanel
End Sub
I’m almost done now; there’s only one thing left to do. I’ve got to call DrawResults() after the DNA is loaded and translated, so I’ll add this line of code to the end of the LoadSequenceBtn_Click handler:
DrawResults()
And we’re done! I can launch the application, load in a file, see the translation visually, and save the results. For example, with a text file containing the string:
ACACGGGCGCGTACAAAAGATGAACTGGGGCCCCGCGCTACCGCCGCCACATTAAAAA
The output I save will look like:
ACACGGGCGCGTACAAAAGATGAACTGGGGCCCCGCGCTACCGCCGCCACATTAAAAA
UGUGCCCGCGCAUGUUUUCUACUUGACCCCGGGGCGCGAUGGCGGCGGUGUAAUUUUU
MetPheSerThr MetAlaAlaVal
On the screen, it looks much prettier, and I can scroll left-to-right to see all of the results.
That’s it for this post. The final code is posted at https://code.msdn.microsoft.com/Release/ProjectReleases.aspx?ProjectName=templeofvb&ReleaseId=1172 -- enjoy!
‘Til next time,
--Matt-