domenica 25 maggio 2014

Istogramma: rappresentazione grafica

Riportiamo di seguito un programma utile qualora si volesse tracciare l'istogramma tridimensionale di una data quantità di riferimento, di cui si suppone di conoscere i valori assunti in diversi istanti di tempo. Si è ipotizzato, in questo caso, di voler rappresentare 20 valori numerici su di un istogramma, posti su due file, in modo dunque che i primi 10 appartenessero ad un primo anno di riferimento, i restanti a quello successivo.

Dopo averi inserito i valori numerici che si vogliono rappresentare, si procede con l'inizializzazione delle variabili necessarie per il disegno del grafico (tra cui numero dei dati inseriti, massimo tra gli stessi, numero di file su cui si posizioneranno le barre, nonché altri riferimenti spaziali e l'oggetto grafico nel quale comparirà l'istogramma):

Public Class Form1

    Private WithEvents PictureBox1 As New PictureBox
    Dim datiGrafico() As Long = {23, 98, 69, 78, 80, 59, 64, 58, 69, 74, 69, 135, 89, 67, 59, 23, 89, 44, 12, 96}
    Dim numeroValori As Integer = 20
    Dim maxValore As Long = CalcolaMassimo(datiGrafico)
    Dim nFile As Integer = 2
    Dim bordo As Integer = 10
    Dim bordoDX As Integer = 30
    Dim larghBarra As Integer = 6
    Dim profRiferimento As Integer = CType(nFile * larghBarra / 2, Integer)
    Dim x1, y1 As Single
    Dim x2, y2 As Single
    Dim scaleX As Single
    Dim scaleY As Single
    Dim gr As Graphics
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        x2 = CType(bordo + numeroValori / nFile * 10 + profRiferimento + bordoDX, Single)
        y2 = CType(bordo + maxValore + profRiferimento + bordo, Single)
        Dim larghezza As Integer = CType(x2 * (Me.Size.Height - 32) / y2, Integer)
        If larghezza <= Me.Size.Width Then
            PictureBox1.Width = larghezza
            PictureBox1.Height = Me.Size.Height - 32
        Else
            PictureBox1.Width = Me.Size.Width
            PictureBox1.Height = CType(y2 * Me.Size.Width / x2, Integer)
        End If
        PictureBox1.Location = New Point(0, 0)
        PictureBox1.Anchor = CType((((System.Windows.Forms.AnchorStyles.Top Or _
                                      System.Windows.Forms.AnchorStyles.Bottom) Or _
                                      System.Windows.Forms.AnchorStyles.Left) Or _
                                      System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)
        Me.Controls.Add(PictureBox1)
   End Sub
   

Viene creata una funzione che restituisca il massimo valore ottenuto sul grafico:

    Private Function CalcolaMassimo(ByVal valore() As Long) As Long
        Dim risultato As Long = 0
        For Each numero As Long In valore
            If numero > risultato Then risultato = numero
        Next
        Return risultato
    End Function
    Private Sub DrawBox(ByVal coloreF As SolidBrush, ByVal coloreL As SolidBrush, ByVal coloreS As SolidBrush, _
                                     ByVal posX As Integer, ByVal posY As Integer, ByVal l As Integer, ByVal p As Integer, _
                                     ByVal h As Integer)
                p = CType(p / 2, Integer)

Vengono determinate le facce, rispettivamente, laterali e superiori di ciascuna barra:

        Dim FacciaVerticale() As Point = {New Point(posX + l, posY + h), New Point(posX + l + p, posY + h + p), _
        New Point(posX + l + p, posY + p), New Point(posX + l, posY)}
        Dim FacciaSuperiore() As Point = {New Point(posX + l, posY + h), New Point(posX + l + p, posY + h + p), _
        New Point(posX + p, posY + h + p), New Point(posX, posY + h)}

A questo punto, è possibile disegnare le barre stesse:

        gr.FillRectangle(coloreF, posX, posY, l, h)
        gr.FillPolygon(coloreL, FacciaVerticale)
        gr.FillPolygon(coloreS, FacciaSuperiore)
    End Sub
    Private Sub PictureBox1_Paint1(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) _
                                   Handles PictureBox1.Paint
        gr = e.Graphics

Vengono definiti i colori di ciascuna faccia dei parallelepipedi, in modo che quelle relative al primo anno di riferimento (dunque, alla prima fila) siano gialle, per il secondo anno arancio e, di seguito, blu. Nell'esempio riportato, comunque, saranno visualizzate solamente due file, che appariranno pertanto, rispettivamente, gialle ed arancio.

        Dim bluF As Color = Color.FromArgb(178, 0, 0, 255) 
        Dim bluS As Color = Color.FromArgb(204, 50, 150, 200) 
        Dim bluL As Color = Color.FromArgb(204, 50, 100, 150) 
        Dim brushBluF As New SolidBrush(bluF)
        Dim brushBluS As New SolidBrush(bluS)
        Dim brushBluL As New SolidBrush(bluL)

        Dim rossoF As Color = Color.FromArgb(217, 247, 100, 30)
        Dim rossoS As Color = Color.FromArgb(204, 209, 111, 81)
        Dim rossoL As Color = Color.FromArgb(204, 170, 97, 74) 
        Dim brushRossoF As New SolidBrush(rossoF)
        Dim brushRossoS As New SolidBrush(rossoS)
        Dim brushRossoL As New SolidBrush(rossoL)

        
        Dim gialloF As Color = Color.FromArgb(217, 255, 255, 50) 
        Dim gialloS As Color = Color.FromArgb(204, 230, 230, 50) 
        Dim gialloL As Color = Color.FromArgb(204, 177, 177, 75) 
        Dim brushGialloF As New SolidBrush(gialloF)
        Dim brushGialloS As New SolidBrush(gialloS)
        Dim brushGialloL As New SolidBrush(gialloL)

Si impone inoltre che, laddove il valore massimo ottenuto sia maggiore di zero, questo venga approssimato per eccesso al multiplo di dieci più vicino, mentre, in caso contrario, si esca immediatamente.

        If (maxValore Mod 10) > 0 Then
            maxValore = (maxValore \ 10) * 10 + 10
        ElseIf maxValore = 0 Then
            Exit Sub
        End If

Oltre a definire numerosi aspetti puramente tecnici, si impone che, relativamente alla seconda fila di parallelepipedi, l'origine sia traslata nel punto (10, 10):
        
        Dim penAssi As New Pen(Color.DarkGray, -1)
        Dim labelFont As New Font("Arial", 3, FontStyle.Regular)
        Dim ColoreTesto As New SolidBrush(Color.Blue)
        x1 = 0
        y1 = 0
        x2 = CType(bordo + numeroValori / nFile * 10 + profRiferimento + bordoDX, Single)
        y2 = bordo + maxValore + profRiferimento + bordo
        scaleX = Me.PictureBox1.Width / (x2 - x1)
        scaleY = Me.PictureBox1.Height / (y2 - y1)
        Dim larghezza As Integer = CType(x2 * (Me.Size.Height - 40) / y2, Integer)
        If larghezza <= Me.Size.Width Then
           PictureBox1.Width = larghezza
            PictureBox1.Height = Me.Size.Height - 40
        Else
            PictureBox1.Width = Me.Size.Width
            PictureBox1.Height = CType(y2 * Me.Size.Width / x2, Integer)
        End If
        
        gr.ScaleTransform(scaleX, -scaleY)
        gr.TranslateTransform(x1, -y2)

dopo aver pulito la casella, viene disegnata la griglia:

        gr.Clear(Color.White)
        Dim colorePareti As Color = Color.FromArgb(218, 221, 223)
        Dim brushPareti As New SolidBrush(colorePareti)
        Dim SistemaDiRiferimento() As Point = { _
        New Point(CInt(x1 + bordo), CInt(y1 + bordo)), _
        New Point(CInt(x2 - profRiferimento - bordoDX), CInt(y1 + bordo)), _
        New Point(CInt(x2 - bordoDX), CInt(y1 + bordo + profRiferimento)), _
        New Point(CInt(x2 - bordoDX), CInt(y2 - bordo)), _
        New Point(CInt(x1 + bordo + profRiferimento), CInt(y2 - bordo)), _
        New Point(CInt(x1 + bordo), CInt(y2 - bordo - profRiferimento))}
        gr.FillPolygon(brushPareti, SistemaDiRiferimento)
        gr.DrawPolygon(Pens.DarkGray, SistemaDiRiferimento)
       For xScan As Single = 0 To CSng((numeroValori / nFile * 10) - 10) Step 10
            gr.DrawLine(penAssi, xScan + bordo + profRiferimento, y1 + bordo + profRiferimento, _
            xScan + bordo + profRiferimento, maxValore + bordo + profRiferimento)
            gr.DrawLine(penAssi, xScan + bordo + profRiferimento, y1 + bordo + profRiferimento, _
            xScan + bordo, y1 + bordo)
        Next xScan
        For yScan As Single = 0 To maxValore - 10 Step 10
            Dim lunghezza As Integer = CInt(numeroValori / nFile * 10)
            gr.DrawLine(penAssi, x1 + bordo + profRiferimento, yScan + bordo + profRiferimento, _
            lunghezza + bordo + profRiferimento, yScan + bordo + profRiferimento)
            gr.DrawLine(penAssi, x1 + bordo, yScan + bordo, x1 + bordo + profRiferimento, _
            yScan + bordo + profRiferimento)
        Next yScan

Vengono tracciati, di seguito, i parallelepipedi che costituiranno l'istogramma:

        For n As Integer = nFile - 1 To 0 Step -1
            For xScan As Single = 0 To CSng((numeroValori / nFile) - 1)
                Dim elemento As Integer = CInt(xScan + n * (numeroValori / nFile))
                If (n Mod 3) = 0 Then
                   DrawBox(brushGialloF, brushGialloL, brushGialloS, CInt(xScan * 10 + 12 + 3 * n), 10 + 3 * n, _
                    larghBarra, larghBarra, CInt(datiGrafico(elemento)))
                ElseIf (n Mod 2) = 0 Then
                    DrawBox(brushBluF, brushBluL, brushBluS, CInt(xScan * 10 + 12 + 3 * n), 10 + 3 * n, _
                    larghBarra, larghBarra, CInt(datiGrafico(elemento)))
                Else
                    DrawBox(brushRossoF, brushRossoL, brushRossoS, CInt(xScan * 10 + 12 + 3 * n), 10 + 3 * n, _
                    larghBarra, larghBarra, CInt(datiGrafico(elemento)))
               End If
           Next xScan
        Next
        gr.ResetTransform()
        gr.ScaleTransform(scaleX, scaleY)
        gr.TranslateTransform(x1, y1)
       

E, a questo punto, vengono inseriti la legenda ed il testo che comparirà su ciascuno dei due assi:

        gr.DrawRectangle(penAssi, x2 - 25, 10, 20, 3 + nFile * 7)
        For i As Integer = nFile - 1 To 0 Step -1
            If (i Mod 3) = 0 Then
                gr.FillRectangle(brushGialloF, x2 - 23, 13 + i * 7, 5, 4)
            ElseIf (i Mod 2) = 0 Then
                gr.FillRectangle(brushBluF, x2 - 23, 13 + i * 7, 5, 4)
            Else
                gr.FillRectangle(brushRossoF, x2 - 23, 13 + i * 7, 5, 4)
            End If
        Next
        For xScan As Single = 0 To CSng(numeroValori / nFile * 10) Step 10
            gr.DrawString(xScan.ToString, labelFont, ColoreTesto, _            CSng(bordo + xScan + 1.7 - 2 * xScan.ToString.Length), y2 - bordo + 3)
        Next xScan
        For yScan As Single = 0 To maxValore Step 10
            gr.DrawString(yScan.ToString, labelFont, ColoreTesto, _            bordo - 2 * yScan.ToString.Length - 3, y2 - bordo - yScan)
        Next yScan     
        For i As Integer = 1 To nFile
            gr.DrawString("200" & i.ToString, labelFont, ColoreTesto, x2 - 18, 6 + 7 * i)
        Next     
        labelFont.Dispose()
        ColoreTesto.Dispose()
        penAssi.Dispose()
        brushBluF.Dispose()
        brushBluL.Dispose()
        brushBluS.Dispose()
        brushRossoF.Dispose()
        brushRossoS.Dispose()
        brushRossoL.Dispose()
        brushGialloF.Dispose()
        brushGialloS.Dispose()
        brushGialloL.Dispose()
        gr = Nothing
    End Sub

Laddove l'utente cambi ridimensioni manualmente la finestra, è opportuno che anche il grafico creato subisca la stessa azioni, mantenendo inalterata la scala:

    Private Sub Form1_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
        Me.Refresh()
    End Sub
    Private Sub Button1_Click(sender As Object, e As EventArgs)
    End Sub
End Class

L'istogramma riportato sarà:


Nessun commento:

Posta un commento