    Imports System
    Imports System.Drawing
    Imports System.Collections
    Imports System.Drawing.Imaging
    Imports System.Drawing.Drawing2D

    Namespace ASPNET.StarterKit.Chart

        '*********************************************************************
        '
        ' BarGraph Class
        '
        ' This class uses GDI+ to render Bar Chart.
        '
        '*********************************************************************

        Public Class BarGraph
            Inherits Chart
            Private _graphLegendSpacer As Single = 15.0F
            Private _labelFontSize As Integer = 7
            Private _legendFontSize As Integer = 9
            Private _legendRectangleSize As Single = 10.0F
            Private _spacer As Single = 5.0F

            ' Overall related members
            Private _backColor As Color
            Private _fontFamily As String
            Private _longestTickValue As String = String.Empty ' Used to calculate max value width
            Private _maxTickValueWidth As Single ' Used to calculate left offset of bar graph
            Private _totalHeight As Single
            Private _totalWidth As Single

            ' Graph related members
            Private _barWidth As Single
            Private _bottomBuffer As Single ' Space from bottom to x axis
            Private _displayBarData As Boolean
            Private _fontColor As Color
            Private _graphHeight As Single
            Private _graphWidth As Single
            Private _maxValue As Single = 0.0F ' = final tick value * tick count
            Private _scaleFactor As Single ' = _maxValue / _graphHeight
            Private _spaceBtwBars As Single ' For now same as _barWidth
            Private _topBuffer As Single ' Space from top to the top of y axis
            Private _xOrigin As Single ' x position where graph starts drawing
            Private _yOrigin As Single ' y position where graph starts drawing
            Private _yLabel As String
            Private _yTickCount As Integer
            Private _yTickValue As Single ' Value for each tick = _maxValue/_yTickCount
            ' Legend related members
            Private _displayLegend As Boolean
            Private _legendWidth As Single
            Private _longestLabel As String = String.Empty ' Used to calculate legend width
            Private _maxLabelWidth As Single = 0.0F

            Public Property FontFamily() As String
                Get
                    Return _fontFamily
                End Get
                Set(ByVal Value As String)
                    _fontFamily = Value
                End Set
            End Property

            Public WriteOnly Property BackgroundColor() As Color
                Set(ByVal Value As Color)
                    _backColor = Value
                End Set
            End Property

            Public WriteOnly Property BottomBuffer() As Integer
                Set(ByVal Value As Integer)
                    _bottomBuffer = Convert.ToSingle(Value)
                End Set
            End Property

            Public WriteOnly Property FontColor() As Color
                Set(ByVal Value As Color)
                    _fontColor = Value
                End Set
            End Property

            Public Property Height() As Integer
                Get
                    Return Convert.ToInt32(_totalHeight)
                End Get
                Set(ByVal Value As Integer)
                    _totalHeight = Convert.ToSingle(Value)
                End Set
            End Property

            Public Property Width() As Integer
                Get
                    Return Convert.ToInt32(_totalWidth)
                End Get
                Set(ByVal Value As Integer)
                    _totalWidth = Convert.ToSingle(Value)
                End Set
            End Property

            Public Property ShowLegend() As Boolean
                Get
                    Return _displayLegend
                End Get
                Set(ByVal Value As Boolean)
                    _displayLegend = Value
                End Set
            End Property

            Public Property ShowData() As Boolean
                Get
                    Return _displayBarData
                End Get
                Set(ByVal Value As Boolean)
                    _displayBarData = Value
                End Set
            End Property

            Public WriteOnly Property TopBuffer() As Integer
                Set(ByVal Value As Integer)
                    _topBuffer = Convert.ToSingle(Value)
                End Set
            End Property

            Public Property VerticalLabel() As String
                Get
                    Return _yLabel
                End Get
                Set(ByVal Value As String)
                    _yLabel = Value
                End Set
            End Property

            Public Property VerticalTickCount() As Integer
                Get
                    Return _yTickCount
                End Get
                Set(ByVal Value As Integer)
                    _yTickCount = Value
                End Set
            End Property

            Public Sub New()
                AssignDefaultSettings()
            End Sub 'New

            Public Sub New(ByVal bgColor As Color)
                AssignDefaultSettings()
                BackgroundColor = bgColor
            End Sub 'New

            '*********************************************************************
            '
            ' This method collects all data points and calculate all the necessary dimensions
            ' to draw the bar graph.  It is the method called before invoking the Draw() method.
            ' labels is the x values.
            ' values is the y values.
            '
            '*********************************************************************

            Public Overloads Sub CollectDataPoints(ByVal labels() As String, ByVal values() As String)
                If labels.Length = values.Length Then
                    Dim i As Integer
                    For i = 0 To labels.Length - 1
                        Dim temp As Single = Convert.ToSingle(values(i))
                        Dim shortLbl As String = MakeShortLabel(labels(i))

                        ' For now put 0.0 for start position and sweep size
                        DataPoints.Add(New ChartItem(shortLbl, labels(i), temp, 0.0F, 0.0F, GetColor(i)))

                        ' Find max value from data; this is only temporary _maxValue
                        If _maxValue < temp Then
                            _maxValue = temp
                        End If
                        ' Find the longest description
                        If _displayLegend Then
                            Dim currentLbl As String = labels(i) + " (" + shortLbl + ")"
                            Dim currentWidth As Single = CalculateImgFontWidth(currentLbl, _legendFontSize, FontFamily)
                            If _maxLabelWidth < currentWidth Then
                                _longestLabel = currentLbl
                                _maxLabelWidth = currentWidth
                            End If
                        End If
                    Next i

                    CalculateTickAndMax()
                    CalculateGraphDimension()
                    CalculateBarWidth(DataPoints.Count, _graphWidth)
                    CalculateSweepValues()
                Else
                    Throw New Exception("X data count is different from Y data count")
                End If
            End Sub 'CollectDataPoints

            '*********************************************************************
            '
            ' Same as above; called when user doesn't care about the x values
            '
            '*********************************************************************

            Public Overloads Sub CollectDataPoints(ByVal values() As String)
                Dim labels As String() = values
                CollectDataPoints(labels, values)
            End Sub 'CollectDataPoints

            '*********************************************************************
            '
            ' This method returns a bar graph bitmap to the calling function.  It is called after
            ' all dimensions and data points are calculated.
            '
            '*********************************************************************

            Public Overrides Function Draw() As Bitmap
                Dim height As Integer = Convert.ToInt32(_totalHeight)
                Dim width As Integer = Convert.ToInt32(_totalWidth)

                Dim bmp As New Bitmap(width, height)
                Dim graph As Graphics = Graphics.FromImage(bmp)
                graph.CompositingQuality = CompositingQuality.HighQuality
                graph.SmoothingMode = SmoothingMode.AntiAlias

                ' Set the background: need to draw one pixel larger than the bitmap to cover all area
                graph.FillRectangle(New SolidBrush(_backColor), -1, -1, bmp.Width + 1, bmp.Height + 1)

                DrawVerticalLabelArea(graph)
                DrawBars(graph)
                DrawXLabelArea(graph)
                If _displayLegend Then
                    DrawLegend(graph)
                End If
                graph.Dispose()

                Return bmp
            End Function 'Draw

            '*********************************************************************
            '
            ' This method draws all the bars for the graph.
            '
            '*********************************************************************

            Private Sub DrawBars(ByVal graph As Graphics)
                Dim brsFont As New SolidBrush(_fontColor)
                Dim valFont As New Font(_fontFamily, _labelFontSize)
                Dim sfFormat As New StringFormat()
                sfFormat.Alignment = StringAlignment.Center
                Dim i As Integer = 0

                ' Draw bars and the value above each bar
                Dim item As ChartItem
                For Each item In DataPoints
                    Dim barBrush As New SolidBrush(item.ItemColor)
                    Dim itemY As Single = _yOrigin + _graphHeight - item.SweepSize

                    ' When drawing, all position is relative to (_xOrigin, _yOrigin)
                    graph.FillRectangle(barBrush, _xOrigin + item.StartPos, itemY, _barWidth, item.SweepSize)

                    ' Draw data value
                    If _displayBarData Then
                        Dim startX As Single = _xOrigin + i * (_barWidth + _spaceBtwBars) ' This draws the value on center of the bar
                        Dim startY As Single = itemY - 2.0F - valFont.Height ' Positioned on top of each bar by 2 pixels
                        Dim recVal As New RectangleF(startX, startY, _barWidth + _spaceBtwBars, valFont.Height)
                        graph.DrawString(item.Value.ToString("#,###.##"), valFont, brsFont, recVal, sfFormat)
                    End If
                    i += 1
                Next item
            End Sub 'DrawBars

            '*********************************************************************
            '
            ' This method draws the y label, tick marks, tick values, and the y axis.
            '
            '*********************************************************************

            Private Sub DrawVerticalLabelArea(ByVal graph As Graphics)
                Dim lblFont As New Font(_fontFamily, _labelFontSize)
                Dim brs As New SolidBrush(_fontColor)
                Dim lblFormat As New StringFormat()
                lblFormat.Alignment = StringAlignment.Near
                Dim pen As New pen(_fontColor)

                ' Draw vertical label at the top of y-axis and place it in the middle top of y-axis
                Dim recVLabel As New RectangleF(0.0F, _yOrigin - 2 * _spacer - lblFont.Height, _xOrigin * 2, lblFont.Height)
                Dim sfVLabel As New StringFormat()
                sfVLabel.Alignment = StringAlignment.Center
                graph.DrawString(_yLabel, lblFont, brs, recVLabel, sfVLabel)

                ' Draw all tick values and tick marks
                Dim i As Integer
                For i = 0 To _yTickCount - 1
                    Dim currentY As Single = _topBuffer + i * _yTickValue / _scaleFactor ' Position for tick mark
                    Dim labelY As Single = currentY - lblFont.Height / 2 ' Place label in the middle of tick
                    Dim lblRec As New RectangleF(_spacer, labelY, _maxTickValueWidth, lblFont.Height)
                    Dim currentTick As Single = _maxValue - i * _yTickValue ' Calculate tick value from top to bottom
                    graph.DrawString(currentTick.ToString("#,###.##"), lblFont, brs, lblRec, lblFormat) ' Draw tick value
                    graph.DrawLine(pen, _xOrigin, currentY, _xOrigin - 4.0F, currentY) ' Draw tick mark
                Next i

                ' Draw y axis
                graph.DrawLine(pen, _xOrigin, _yOrigin, _xOrigin, _yOrigin + _graphHeight)
            End Sub 'DrawVerticalLabelArea

            '*********************************************************************
            '
            ' This method draws x axis and all x labels
            '
            '*********************************************************************

            Private Sub DrawXLabelArea(ByVal graph As Graphics)
                Dim lblFont As New Font(_fontFamily, _labelFontSize)
                Dim brs As New SolidBrush(_fontColor)
                Dim lblFormat As New StringFormat()
                lblFormat.Alignment = StringAlignment.Center
                Dim pen As New pen(_fontColor)

                ' Draw x axis
                graph.DrawLine(pen, _xOrigin, _yOrigin + _graphHeight, _xOrigin + _graphWidth, _yOrigin + _graphHeight)

                Dim currentX As Single
                Dim currentY As Single = _yOrigin + _graphHeight + 2.0F ' All x labels are drawn 2 pixels below x-axis
                Dim labelWidth As Single = _barWidth + _spaceBtwBars ' Fits exactly below the bar
                Dim i As Integer = 0

                ' Draw x labels
                Dim item As ChartItem
                For Each item In DataPoints
                    currentX = _xOrigin + i * labelWidth
                    Dim recLbl As New RectangleF(currentX, currentY, labelWidth, lblFont.Height)
                    Dim lblString As String
                    If _displayLegend Then
                        lblString = item.Label
                    Else
                        lblString = item.Description
                    End If
                    graph.DrawString(lblString, lblFont, brs, recLbl, lblFormat)
                    i += 1
                Next item
            End Sub 'DrawXLabelArea

            '*********************************************************************
            '
            ' This method determines where to place the legend box.
            ' It draws the legend border, legend description, and legend color code.
            '
            '*********************************************************************

            Private Sub DrawLegend(ByVal graph As Graphics)
                Dim lblFont As New Font(_fontFamily, _legendFontSize)
                Dim brs As New SolidBrush(_fontColor)
                Dim lblFormat As New StringFormat()
                lblFormat.Alignment = StringAlignment.Near
                Dim pen As New pen(_fontColor)

                ' Calculate Legend drawing start point
                Dim startX As Single = _xOrigin + _graphWidth + _graphLegendSpacer
                Dim startY As Single = _yOrigin

                Dim xColorCode As Single = startX + _spacer
                Dim xLegendText As Single = xColorCode + _legendRectangleSize + _spacer
                Dim legendHeight As Single = 0.0F
                Dim i As Integer
                For i = 0 To DataPoints.Count - 1
                    Dim point As ChartItem = DataPoints(i)
                    Dim [text] As String = point.Description + " (" + point.Label + ")"
                    Dim currentY As Single = startY + _spacer + i * (lblFont.Height + _spacer)
                    legendHeight += lblFont.Height + _spacer

                    ' Draw legend description
                    graph.DrawString([text], lblFont, brs, xLegendText, currentY, lblFormat)

                    ' Draw color code
                    graph.FillRectangle(New SolidBrush(DataPoints(i).ItemColor), xColorCode, currentY + 3.0F, _legendRectangleSize, _legendRectangleSize)
                Next i

                ' Draw legend border
                graph.DrawRectangle(pen, startX, startY, _legendWidth, legendHeight + _spacer)
            End Sub 'DrawLegend

            '*********************************************************************
            '
            ' This method calculates all measurement aspects of the bar graph from the given data points
            '
            '*********************************************************************

            Private Sub CalculateGraphDimension()
                FindLongestTickValue()

                ' Need to add another character for spacing; this is not used for drawing, just for calculation
                _longestTickValue += "0"
                _maxTickValueWidth = CalculateImgFontWidth(_longestTickValue, _labelFontSize, FontFamily)
                Dim leftOffset As Single = _spacer + _maxTickValueWidth
                Dim rtOffset As Single = 0.0F

                If _displayLegend Then
                    _legendWidth = _spacer + _legendRectangleSize + _spacer + _maxLabelWidth + _spacer
                    rtOffset = _graphLegendSpacer + _legendWidth + _spacer
                Else
                    rtOffset = _spacer ' Make graph in the middle
                End If
                _graphHeight = _totalHeight - _topBuffer - _bottomBuffer ' Buffer spaces are used to print labels
                _graphWidth = _totalWidth - leftOffset - rtOffset
                _xOrigin = leftOffset
                _yOrigin = _topBuffer

                ' Once the correct _maxValue is determined, then calculate _scaleFactor
                _scaleFactor = _maxValue / _graphHeight
            End Sub 'CalculateGraphDimension

            '*********************************************************************
            '
            ' This method determines the longest tick value from the given data points.
            ' The result is needed to calculate the correct graph dimension.
            '
            '*********************************************************************

            Private Sub FindLongestTickValue()
                Dim currentTick As Single
                Dim tickString As String
                Dim i As Integer
                For i = 0 To _yTickCount - 1
                    currentTick = _maxValue - i * _yTickValue
                    tickString = currentTick.ToString("#,###.##")
                    If _longestTickValue.Length < tickString.Length Then
                        _longestTickValue = tickString
                    End If
                Next i
            End Sub 'FindLongestTickValue

            '*********************************************************************
            '
            ' This method calculates the image width in pixel for a given text
            '
            '*********************************************************************

            Private Function CalculateImgFontWidth(ByVal [text] As String, ByVal size As Integer, ByVal family As String) As Single
                Dim bmp As Bitmap = Nothing
                Dim graph As Graphics = Nothing
                Dim font As New font(family, size)

                ' Calculate the size of the string.
                bmp = New Bitmap(1, 1, PixelFormat.Format32bppArgb)
                graph = Graphics.FromImage(bmp)
                Dim oSize As SizeF = graph.MeasureString([text], font)

                graph.Dispose()
                bmp.Dispose()

                Return oSize.Width
            End Function 'CalculateImgFontWidth

            '*********************************************************************
            '
            ' This method creates abbreviation from long description; used for making legend
            '
            '*********************************************************************

            Private Function MakeShortLabel(ByVal [text] As String) As String
                Dim label As String = [text]
                If [text].Length > 2 Then
                    Dim midPostition As Integer = Convert.ToInt32(Math.Floor(([text].Length / 2)))
                    label = [text].Substring(0, 1) + [text].Substring(midPostition, 1) + [text].Substring([text].Length - 1, 1)
                End If
                Return label
            End Function 'MakeShortLabel

            '*********************************************************************
            '
            ' This method calculates the max value and each tick mark value for the bar graph.
            '
            '*********************************************************************

            Private Sub CalculateTickAndMax()
                Dim tempMax As Single = 0.0F

                ' Give graph some head room first about 10% of current max
                _maxValue *= 1.1F

                If _maxValue <> 0.0F Then
                    ' Find a rounded value nearest to the current max value
                    ' Calculate this max first to give enough space to draw value on each bar
                    Dim exp As Double = Convert.ToDouble(Math.Floor(Math.Log10(_maxValue)))
                    tempMax = Convert.ToSingle((Math.Ceiling((_maxValue / Math.Pow(10, exp))) * Math.Pow(10, exp)))
                Else
                    tempMax = 1.0F
                End If
                ' Once max value is calculated, tick value can be determined; tick value should be a whole number
                _yTickValue = tempMax / _yTickCount
                Dim expTick As Double = Convert.ToDouble(Math.Floor(Math.Log10(_yTickValue)))
                _yTickValue = Convert.ToSingle((Math.Ceiling((_yTickValue / Math.Pow(10, expTick))) * Math.Pow(10, expTick)))

                ' Re-calculate the max value with the new tick value
                _maxValue = _yTickValue * _yTickCount
            End Sub 'CalculateTickAndMax

            '*********************************************************************
            '
            ' This method calculates the height for each bar in the graph
            '
            '*********************************************************************

            Private Sub CalculateSweepValues()
                ' Called when all values and scale factor are known
                ' All values calculated here are relative from (_xOrigin, _yOrigin)
                Dim i As Integer = 0
                Dim item As ChartItem
                For Each item In DataPoints
                    ' This implementation does not support negative value
                    If item.Value >= 0 Then
                        item.SweepSize = item.Value / _scaleFactor
                    End If
                    ' (_spaceBtwBars/2) makes half white space for the first bar
                    item.StartPos = _spaceBtwBars / 2 + i * (_barWidth + _spaceBtwBars)
                    i += 1
                Next item
            End Sub 'CalculateSweepValues

            '*********************************************************************
            '
            ' This method calculates the width for each bar in the graph
            '
            '*********************************************************************

            Private Sub CalculateBarWidth(ByVal dataCount As Integer, ByVal barGraphWidth As Single)
                ' White space between each bar is the same as bar width itself
                _barWidth = barGraphWidth / (dataCount * 2) ' Each bar has 1 white space
                _spaceBtwBars = _barWidth
            End Sub 'CalculateBarWidth

            '*********************************************************************
            '
            ' This method assigns default value to the bar graph properties and is only
            ' called from BarGraph constructors
            '
            '*********************************************************************

            Private Sub AssignDefaultSettings()
                ' default values
                _totalWidth = 700.0F
                _totalHeight = 450.0F
                _fontFamily = "Verdana"
                _backColor = Color.White
                _fontColor = Color.Black
                _topBuffer = 30.0F
                _bottomBuffer = 30.0F
            End Sub 'AssignDefaultSettings
        End Class 'BarGraph
    End Namespace 'ASPNetStarterKit.Charting