﻿Imports System.ComponentModel
Imports System.Drawing.Drawing2D
Imports System.Drawing.Imaging

Namespace RyChart
    ''' <summary>
    ''' A control that displays a point chart.
    ''' </summary>
    Public Class PointChart
        Inherits Control

#Region "DataSet"
        Public Class DataSet

#Region "Events"
            Public Event Changed(ByVal sender As DataSet)

            Protected Friend Overridable Sub OnChanged()
                RaiseEvent Changed(Me)
            End Sub
#End Region

#Region "Constructor"
            ''' <summary>
            ''' Creates a new set of data for a point chart called "Data Set", in black.
            ''' </summary>
            Public Sub New()
                Me.New("Data Set", Drawing.Color.Black)
            End Sub

            ''' <summary>
            ''' Creates a new set of data for a point chart, with the specified name.
            ''' </summary>
            ''' <param name="name">The name of the data set.</param>
            Public Sub New(ByVal name As String)
                Me.New(name, Drawing.Color.Black)
            End Sub

            ''' <summary>
            ''' Creates a new set of data for a point chart, with the specified name, in the specified color.
            ''' </summary>
            ''' <param name="name">The name of the data set.</param>
            ''' <param name="color">The color in which to display the line representing the data.</param>
            Public Sub New(ByVal name As String, ByVal color As Color)
                _name = name
                _color = color
            End Sub
#End Region

#Region "Properties"
            Private _color As Color
            Private _name As String
            Private _data As New PointChartDataPointCollection(Me)

            ''' <summary>
            ''' Gets or sets the color of the line representing this data set on the <see cref="PointChart" />.
            ''' </summary>
            Public Property Color() As Color
                Get
                    Return _color
                End Get
                Set(ByVal value As Color)
                    _color = value

                    Me.OnChanged()
                End Set
            End Property

            ''' <summary>
            ''' Gets or sets the name of this set of data.
            ''' The name may be displayed in the legend of the <see cref="PointChart" />.
            ''' </summary>
            Public Property Name() As String
                Get
                    Return _name
                End Get
                Set(ByVal value As String)
                    _name = value

                    Me.OnChanged()
                End Set
            End Property

            ''' <summary>
            ''' Gets a collection of data to be displayed on the parent <see cref="PointChart" />.
            ''' </summary>
            <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
            Public ReadOnly Property Data() As PointChartDataPointCollection
                Get
                    Return _data
                End Get
            End Property
#End Region

        End Class
#End Region

#Region "Constructor"
        Public Sub New()
            Me.DoubleBuffered = True
            Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
                        ControlStyles.UserPaint Or
                        ControlStyles.OptimizedDoubleBuffer Or
                        ControlStyles.Opaque Or
                        ControlStyles.ResizeRedraw, True)
            Me.Padding = New Padding(40)
        End Sub
#End Region

#Region "Properties"
        Private _data As New PointChartDataSetCollection(Me)
        Private _xLabel As String = "X Label"
        Private _xLabels As New PointChartLabelCollection(Me)
        Private _yLabel As String = "Y Label"
        Private _minimum, _maximum As Integer
        Private _titleFont As Font = New Font(Me.Font.FontFamily, 20.0!, FontStyle.Bold)
        Private _yResolution As Integer = 0

        ''' <summary>
        ''' Gets a collection of sets of data to be displayed on the point chart.
        ''' </summary>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
         Description("The data displayed on the point chart."),
         Category("Point Chart"), RefreshProperties(RefreshProperties.All)>
        Public ReadOnly Property Data() As PointChartDataSetCollection
            Get
                Return _data
            End Get
        End Property

        ''' <summary>
        ''' Gets a collection of labels to be displayed on the x-axis of the point chart.
        ''' </summary>
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
         Description("The labels displayed on the x-axis of the point chart."),
         Category("Point Chart")>
        Public ReadOnly Property XLabels() As PointChartLabelCollection
            Get
                Return _xLabels
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets the label for the chart's x-axis.
        ''' </summary>
        ''' <exception cref="ArgumentNullException">Thrown if the label is set to Nothing.</exception>
        <Description("The label for the chart's x-axis."),
         Category("Point Chart")>
        Public Property XLabel() As String
            Get
                Return _xLabel
            End Get
            Set(ByVal value As String)
                If value Is Nothing Then Throw New ArgumentNullException("value")

                _xLabel = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the label for the chart's y-axis.
        ''' </summary>
        ''' <exception cref="ArgumentNullException">Thrown if the label is set to Nothing.</exception>
        <Description("The label for the chart's y-axis."),
         Category("Point Chart")>
        Public Property YLabel() As String
            Get
                Return _yLabel
            End Get
            Set(ByVal value As String)
                If value Is Nothing Then Throw New ArgumentNullException("value")

                _yLabel = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the font in which to display the chart's title.
        ''' </summary>
        ''' <exception cref="ArgumentNullException">Thrown if the title font is set to Nothing.</exception>
        <Description("The font of the chart's title."),
         Category("Point Chart")>
        Public Property TitleFont() As Font
            Get
                Return _titleFont
            End Get
            Set(ByVal value As Font)
                If value Is Nothing Then Throw New ArgumentNullException("value")

                _titleFont = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the minimum y-value on the chart.
        ''' </summary>
        ''' <remarks>If Minimum is more than or equal to <see cref="Maximum" />, the minimum and maximum values are retrieved from the data.</remarks>
        <Description("The minimum y-value on the chart. Set to more than or equal to Maximum to automatically calculate bounds."),
         Category("Point Chart"), RefreshProperties(RefreshProperties.All)>
        Public Property Minimum() As Integer
            Get
                If _minimum < _maximum Then
                    Return _minimum
                Else
                    Dim minVal As Integer = If(Me.Data.Count > 0 AndAlso Me.Data(0).Data.Count > 0, Me.Data(0).Data(0), 0)

                    For Each ds As DataSet In Me.Data
                        For Each i As Integer In ds.Data
                            If i < minVal Then minVal = i
                        Next
                    Next

                    Return minVal
                End If
            End Get
            Set(ByVal value As Integer)
                _minimum = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the maximum y-value on the chart.
        ''' </summary>
        ''' <remarks>If <see cref="Minimum" /> is more than or equal to Maximum, the minimum and maximum values are retrieved from the data.</remarks>
        <Description("The maximum y-value on the chart. Set to less than or equal to Minimum to automatically calculate bounds."),
         Category("Point Chart"), RefreshProperties(RefreshProperties.All)>
        Public Property Maximum() As Integer
            Get
                If _minimum < _maximum Then
                    Return _maximum
                Else
                    Dim maxVal As Integer = If(Me.Data.Count > 0 AndAlso Me.Data(0).Data.Count > 0, Me.Data(0).Data(0), 0)

                    For Each ds As DataSet In Me.Data
                        For Each i As Integer In ds.Data
                            If i > maxVal Then maxVal = i
                        Next
                    Next

                    Return maxVal
                End If
            End Get
            Set(ByVal value As Integer)
                _maximum = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the frequency of the ticks appearing on the y-axis of the chart.
        ''' </summary>
        ''' <exception cref="ArgumentOutOfRangeException">Thrown when attempting to set the frequency to a negative value.</exception>
        ''' <remarks>Set to zero to automatically decide based on the height of the chart.</remarks>
        <Description("The frequency of the ticks appearing on the y-axis of the chart. Set to 0 to decide automatically."),
         Category("Point Chart")>
        Public Property YResolution() As Integer
            Get
                If _yResolution = 0 Then
                    Return Math.Max(1, CInt((Me.Maximum - Me.Minimum) / (Me.ClientSize.Height - Me.Padding.Top - Me.Padding.Bottom) * Me.Font.Height))
                Else
                    Return _yResolution
                End If
            End Get
            Set(ByVal value As Integer)
                If value < 0 Then Throw New ArgumentOutOfRangeException("value")

                _yResolution = value

                Me.Invalidate()
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the chart's title.
        ''' </summary>
        <Description("The chart's title."),
         Category("Point Chart")>
        Public Overrides Property Text() As String
            Get
                Return MyBase.Text
            End Get
            Set(ByVal value As String)
                MyBase.Text = value

                Me.Invalidate()
            End Set
        End Property
#End Region

#Region "Drawing"
        Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
            Dim g As Graphics = e.Graphics

            g.DrawImageUnscaled(Me.GetBitmap(Me.BackColor), 0, 0)
        End Sub

        ''' <summary>
        ''' Draws the graph's axes and axis labels.
        ''' </summary>
        ''' <param name="g">The graphics context on which the axes are to be drawn.</param>
        Protected Overridable Sub DrawAxes(ByVal g As Graphics)
            Dim xLabelSize As SizeF = g.MeasureString(Me.XLabel, Me.Font)
            Dim yLabelSize As SizeF = g.MeasureString(Me.YLabel, Me.Font)

            Using foreBrush As New SolidBrush(Me.ForeColor)
                'Draw the ticks on the y-axis and their labels:
                For i As Integer = Me.Minimum To Me.Maximum Step Me.YResolution
                    Dim y As Integer = CInt(Me.ClientSize.Height - Me.Padding.Bottom - (Me.ClientSize.Height - Me.Padding.Top - Me.Padding.Bottom) / (Me.Maximum - Me.Minimum) * (i - Me.Minimum))

                    If i > Me.Minimum Then g.DrawLine(Pens.Silver, Me.Padding.Left, y, Me.ClientSize.Width - Me.Padding.Right, y)

                    g.DrawString(i.ToString(), Me.Font, foreBrush, Me.ClientSize.Width - Me.Padding.Right + 2,
                                 y - Me.Font.Height / 2.0!)
                Next

                'Draw the ticks on the x-axis and their labels:
                For i As Integer = 0 To Me.XLabels.Count - 1
                    Dim x As Integer = CInt(Me.Padding.Left + (Me.ClientSize.Width - Me.Padding.Left - Me.Padding.Right) / (Me.XLabels.Count - 1) * i)
                    Dim labelSize As SizeF = g.MeasureString(Me.XLabels(i), Me.Font)

                    If i > 0 Then g.DrawLine(Pens.Gray, x, Me.Padding.Top, x, Me.ClientSize.Height - Me.Padding.Bottom)

                    g.DrawLine(Pens.Black, x, Me.ClientSize.Height - Me.Padding.Bottom, x, Me.ClientSize.Height - Me.Padding.Bottom + 5)
                    g.DrawString(Me.XLabels(i), Me.Font, foreBrush, x - labelSize.Width / 2.0!, Me.ClientSize.Height - Me.Padding.Bottom + 5)
                Next

                'Draw the x-axis and its label:
                g.DrawLine(Pens.Black, Me.Padding.Left, Me.ClientSize.Height - Me.Padding.Bottom,
                           Me.ClientSize.Width - Me.Padding.Right, Me.ClientSize.Height - Me.Padding.Bottom)
                g.DrawString(Me.XLabel, Me.Font, foreBrush,
                             (Me.ClientSize.Width - Me.Padding.Left - Me.Padding.Right) / 2.0! -
                              xLabelSize.Width / 2.0! + Me.Padding.Left, Me.ClientSize.Height -
                             Me.Padding.Bottom + 10 + Me.Font.Height)

                'Draw the y-axis and its label:
                g.DrawLine(Pens.Black, Me.Padding.Left, Me.Padding.Top, Me.Padding.Left, Me.ClientSize.Height - Me.Padding.Bottom)
                g.TranslateTransform(Me.Padding.Left - 5 - yLabelSize.Height, (Me.ClientSize.Height - Me.Padding.Top - Me.Padding.Bottom) / 2.0! + yLabelSize.Width / 2.0! + Me.Padding.Top)
                g.RotateTransform(-90.0!)
                g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAliasGridFit
                g.DrawString(Me.YLabel, Me.Font, foreBrush, 0.0!, 0.0!)
                g.ResetTransform()
                g.TextRenderingHint = Drawing.Text.TextRenderingHint.ClearTypeGridFit
            End Using
        End Sub

        ''' <summary>
        ''' Draws the graph's data.
        ''' </summary>
        ''' <param name="g">The graphics context upon which the data is to be displayed.</param>
        Protected Overridable Sub DrawData(ByVal g As Graphics)
            g.SmoothingMode = SmoothingMode.HighQuality

            For Each ds As DataSet In Me.Data
                Dim lastPoint As Point = Point.Empty

                Using p As New Pen(ds.Color, 1), b As New SolidBrush(ds.Color)
                    For i As Integer = 0 To ds.Data.Count - 1
                        Dim currentPoint As New Point(CInt(Me.Padding.Left + (Me.ClientSize.Width - Me.Padding.Left - Me.Padding.Right) / (ds.Data.Count - 1) * i), CInt(Me.ClientSize.Height - Me.Padding.Bottom - (Me.ClientSize.Height - Me.Padding.Top - Me.Padding.Bottom) * (ds.Data(i) - Me.Minimum) / (Me.Maximum - Me.Minimum)))

                        If i > 0 Then g.DrawLine(p, lastPoint, currentPoint)

                        g.FillEllipse(b, currentPoint.X - 2, currentPoint.Y - 2, 4, 4)

                        lastPoint = currentPoint
                    Next
                End Using
            Next

            g.SmoothingMode = SmoothingMode.Default
        End Sub

        ''' <summary>
        ''' Draws the graph's legend.
        ''' </summary>
        ''' <param name="g">The graphics context upon which the legend is to be drawn.</param>
        Protected Overridable Sub DrawLegend(ByVal g As Graphics)
            'Override in derived control to draw
        End Sub

        ''' <summary>
        ''' Draws the graph's title.
        ''' </summary>
        ''' <param name="g">The graphics context upon which the title is to be drawn.</param>
        Protected Overridable Sub DrawTitle(ByVal g As Graphics)
            Dim titleSize As SizeF = g.MeasureString(Me.Text, Me.TitleFont)

            Using foreBrush As New SolidBrush(Me.ForeColor)
                g.DrawString(Me.Text, Me.TitleFont, foreBrush, Me.ClientSize.Width / 2.0! - titleSize.Width / 2.0!, 5.0!)
            End Using
        End Sub

        ''' <summary>
        ''' Draws additionnal adornments on the graph.
        ''' </summary>
        ''' <param name="g">The graphics context used for drawing.</param>
        Protected Overridable Sub DrawOverlay(ByVal g As Graphics)
            g.DrawRectangle(Pens.Black, 0, 0, Me.ClientSize.Width - 1, Me.ClientSize.Height - 1)
        End Sub

        ''' <summary>
        ''' Renders the point chart to a bitmap image that has the same size as the control and a white background.
        ''' </summary>
        Public Function GetBitmap() As Bitmap
            Return Me.GetBitmap(Color.White)
        End Function

        ''' <summary>
        ''' Renders the point chart to a bitmap image that has the same size as the control and the specified background color.
        ''' </summary>
        ''' <param name="backgroundColor">The background color of the image to create.</param>
        Public Function GetBitmap(ByVal backgroundColor As Color) As Bitmap
            GetBitmap = New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height, PixelFormat.Format24bppRgb)

            Using g As Graphics = Graphics.FromImage(GetBitmap)
                g.Clear(backgroundColor)

                'First, draw the axes and their labels:
                Me.DrawAxes(g)

                'Next, draw the graph's actual data:
                Me.DrawData(g)

                'Next, draw the legend:
                Me.DrawLegend(g)

                'Next, draw the title:
                Me.DrawTitle(g)

                'And finally, paint additional adornments:
                Me.DrawOverlay(g)
            End Using

            'Done!
        End Function
#End Region

    End Class

#Region "Collection Types"
    ''' <summary>
    ''' Represents a collection of data sets to be displayed on a <see cref="PointChart" />.
    ''' </summary>
    Public Class PointChartDataSetCollection
        Inherits System.Collections.ObjectModel.Collection(Of PointChart.DataSet)

#Region "Properties"
        Private _parent As PointChart

        ''' <summary>
        ''' Gets the parent point chart of this collection of sets of data.
        ''' </summary>
        Public ReadOnly Property Parent() As PointChart
            Get
                Return _parent
            End Get
        End Property
#End Region

#Region "Constructor"
        ''' <summary>
        ''' Creates a new collection of sets of data with the specified parent point chart.
        ''' </summary>
        ''' <param name="parent">The parent point chart of the collection.</param>
        Public Sub New(ByVal parent As PointChart)
            _parent = parent
        End Sub
#End Region

#Region "List Access"
        Protected Overrides Sub ClearItems()
            For Each ds As PointChart.DataSet In Me
                RemoveHandler ds.Changed, AddressOf Me.OnItemChanged
            Next

            MyBase.ClearItems()
        End Sub

        Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As PointChart.DataSet)
            RemoveHandler Me(index).Changed, AddressOf Me.OnItemChanged
            AddHandler item.Changed, AddressOf Me.OnItemChanged

            MyBase.SetItem(index, item)
        End Sub

        Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As PointChart.DataSet)
            AddHandler item.Changed, AddressOf Me.OnItemChanged

            MyBase.InsertItem(index, item)
        End Sub

        Protected Overrides Sub RemoveItem(ByVal index As Integer)
            RemoveHandler Me(index).Changed, AddressOf Me.OnItemChanged

            MyBase.RemoveItem(index)
        End Sub
#End Region

#Region "Changed Handler"
        Protected Overridable Sub OnItemChanged(ByVal sender As PointChart.DataSet)
            Me.Parent.Invalidate()
        End Sub
#End Region

    End Class

    ''' <summary>
    ''' Represents a collection of data points to be displayed on a <see cref="PointChart" />.
    ''' </summary>
    Public Class PointChartDataPointCollection
        Inherits System.Collections.ObjectModel.Collection(Of Integer)

#Region "Properties"
        Private _parent As PointChart.DataSet

        ''' <summary>
        ''' Gets the parent of this collection of data.
        ''' </summary>
        Public ReadOnly Property Parent() As PointChart.DataSet
            Get
                Return _parent
            End Get
        End Property
#End Region

#Region "Constructor"
        ''' <summary>
        ''' Creates a new collection of data with the specified parent data set.
        ''' </summary>
        ''' <param name="parent">The parent data set of the collection.</param>
        Public Sub New(ByVal parent As PointChart.DataSet)
            _parent = parent
        End Sub
#End Region

#Region "List Access"
        Protected Overrides Sub ClearItems()
            MyBase.ClearItems()

            Me.Parent.OnChanged()
        End Sub

        Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As Integer)
            MyBase.SetItem(index, item)

            Me.Parent.OnChanged()
        End Sub

        Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As Integer)
            MyBase.InsertItem(index, item)

            Me.Parent.OnChanged()
        End Sub

        Protected Overrides Sub RemoveItem(ByVal index As Integer)
            MyBase.RemoveItem(index)

            Me.Parent.OnChanged()
        End Sub
#End Region

    End Class

    ''' <summary>
    ''' Represents a collection of labels to displayed on an axis of a <see cref="PointChart" />.
    ''' </summary>
    Public Class PointChartLabelCollection
        Inherits System.Collections.ObjectModel.Collection(Of String)

#Region "Properties"
        Private _parent As PointChart

        ''' <summary>
        ''' Gets the parent of this collection of labels.
        ''' </summary>
        Public ReadOnly Property Parent() As PointChart
            Get
                Return _parent
            End Get
        End Property
#End Region

#Region "Constructor"
        ''' <summary>
        ''' Creates a new collection of labels with the specified parent point chart.
        ''' </summary>
        ''' <param name="parent">The parent point chart of the collection.</param>
        Public Sub New(ByVal parent As PointChart)
            _parent = parent
        End Sub
#End Region

#Region "List Access"
        Protected Overrides Sub ClearItems()
            MyBase.ClearItems()

            Me.Parent.Invalidate()
        End Sub

        Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As String)
            MyBase.SetItem(index, item)

            Me.Parent.Invalidate()
        End Sub

        Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As String)
            MyBase.InsertItem(index, item)

            Me.Parent.Invalidate()
        End Sub

        Protected Overrides Sub RemoveItem(ByVal index As Integer)
            MyBase.RemoveItem(index)

            Me.Parent.Invalidate()
        End Sub
#End Region
    End Class
#End Region

End Namespace