Introduction
The accompanying application attempts to demonstrate the use of the MSChart Control in a typical SME Line of Business Application. It covers the use of Pyramid, Pie and Column Graphs as they would typically be used in a business management type application.
Background
Our company specialises in custom Business applications. Basically we develop a generalised application, and then customise that application to meet requirements for specific customers. We rewrite these general apps on a four year cycle, and, this year being a major rewrite year, I was researching and testing various components and technologies that we would likely be using in the upgrades. Previously, we had been using an in-house charting control, but this control has become rather klunky and bloated over the years, and so we wanted to look for an alternative. Commercial Chart controls such as Dundas were way out of budget, and so I came across Microsoft's free Chart control, MSChart for .NET.
Requirements
To run the sample application requires that you have the MSChart Control installed on your machine, which can be downloaded from MSChart. This control requires your installation of VS 2008 to be updated to SP1. It is also better to have the VS Add-On for VS 2008 which can be found at MSChart Add-On For VS 2008. This Add-On requires the .NET Framework to be updated to .NET 3.5 SP1. The database for the application is included with the VS project, you simply need to set the ConString
setting in My.Settings
to point to the correct file location.
Application Details
As this article is about using the MSChart Control rather than the application as such, I will just give a brief overview of the application, and how it works. The application opens up with a pyramid graph showing the percentage of the company's Gross Sales divided into 6 different product categories. The application defaults, on opening, to the current months figures. On the left are 2 selection panels, and a DataGrid
. The top selection panel gives the user the option of selecting which month's figures to view, while the second gives the user the choice of whether to view the categories as a percent of Gross Sales, or as Total Retail Value. The Datagrid
shows the actual figures according to the various selections. On the right is a vertical menu which allows the user to Copy the Graph to the clipboard, Save the graph as an image or Print a report showing the graph.
When the user moves the mouse over one of the graph's Data Points, or Legend Items, the graph will highlight the particular Category. If the user clicks on the selected Data Point, then the graph will change to a Doughnut graph, showing the percentage of each Product within the chosen Category, and the second selection panel will give an added choice of showing Unit Sales, as well as Retail Value and Percent of Sales. The datagrid shows the values for each product in the Category. A sliding trackbar comes into view which enables the user to group products that have a percentage value less than the selected amount. Once again, the graph can be Copied, Saved as image or Printed as a report. A button also becomes visible allowing the user to return to the Category view.
On selecting a product in the pie graph, a second form will open, which will show the user a Column graph showing daily sales for the product over the past 3 full months as well as the current month. On the top left is an information panel showing the various figures for the product, and on the bottom left is a small Column graph showing total unit sales for the product for those months. If the user clicks any month on the small graph, the main graph will change to reflect only that specific month's figures. A button will come into view which will allow the user to reset the main graph to the default. Once again, the user can Copy, Save or Print a report.
Basics of the MSChart Control
Once installed on your machine, the chart requires a reference to System.Windows.Forms.DataVisualization
in your project. Adding a chart to your form is then just a matter of dragging a chart on to your form from the ToolBox, under Data heading. The control can basically be divided up into 3 sections, The Chart Area, the Title and the Legend. The Title is self explanatory, this being where you let the user know what the graph is all about. The titles can be set in properties by selecting the Titles collection and setting various properties, such as Font
, ForeColor
for each title in the collection.
Graph Appearance
The main appearance elements of the graph can be set using the properties panel. Using BackColor
, BackSecondaryColor
and BackHatchStyle
, you can come up with some fairly decent, and consistent designs. An interesting property is the BorderSkin
property which allows you to define various borders, and set various properties for the border. The main graph in the demo app uses a SkinStyle
of FrameTitle1
with a bluish BackColor
and a BackGradientStyle
of VerticalCentre
. One of the most important things I found was to keep properties such as BackGradientStyle
consistent throughout the design to get a better look. I found that setting CharttArea.BackColor
and Legend.BackColor
to Transparent
gives the best results. To select the palette used for painting the Data Points, you could use a predefined Palette using the Palette
property, or to define your own Palette
, you can use the PaletteCustomColor
Collection to define various colours, the demo app main graph using a selection of blue shades. If you do define your own Palette then you need to set Palette
property to None
, otherwise it uses the Palette
property as default. There are two properties which can be used to determine the quality of various elements. Text rendering quality can be set with the TextAntiAliasingQualityProperty
, and Shadow smoothing can be set using the IsSoftShadows enum
.
Graph Features
Series
Series are where it all happens with Chart control. A series consists of a collection of data points. A graph may have an unlimited number of series, and each series can be set to a different chart type (within reason), and can have its appearance properties set separately. The data points in a series can also have their individual appearance properties set, in which case the data point's appearance takes preference over the owning series. Each data point consists of an X value that determines where the data point is placed on the graph, and one or more Y values, dependant on the type of graph. Each type of graph has custom attributes which can be set with the CustomAttributes
Property. An example of setting Custom attributes at runtime is shown in the Function GraphSetUp
in frmChartExample
where we set the Group Threshold Limit by sliding the trackbar on the form.
mscSales.Series(0)("CollectedThreshold") = tbGroupPercentage.Value
There are numerous ways to populate your series, depending on how your data comes to you. The possible data sources include DataView
, DataReader
, DataSet
, DataRow
, Arrays
, Lists DataAdapters
as well as other sources, in fact any object that implements the IEnumerable
Interface can be used. In the demo app, I have used two different types of data sources.
In the main graph, I create a list of custom DataPoints
. I then iterate through the list manually adding each X and Y value on each iteration.
For intDataPoints As Integer = 0 To lstDataPoints.Count - 1
mscSales.Series(0).Points.AddXY(lstDataPoints(intDataPoints).DataKey, _
Format(lstDataPoints(intDataPoints).DataValue, "#,##0.00"))
Dim strRow() As String = {lstDataPoints(intDataPoints).DataKey, _
Format(lstDataPoints(intDataPoints).DataValue, "#,##0.00")}
decGross += lstDataPoints(intDataPoints).DataValue
dgvDetails.Rows.Add(strRow)
Next
On the second form, I have bound the main graph to a DataTable
returned from a function of the Product
class. All you need do here is set the DataSource
property to the DataTable
, and then set your X
and Y ValueMembers
properties to the correct columns in your DataTable
, and call DataBind
on your Chart Control, like this:
Dim dtMasterSales As DataTable = prdCurrent.DailySales_
(dteStartDate, dteEndDate, My.Settings.ConString)
mscDailySales.DataSource = dtMasterSales
mscDailySales.Series(0).XValueMember = "Date"
mscDailySales.Series(0).YValueMembers = "UnitsSold"
mscDailySales.DataBind()
Graph Interaction
There are many functions that can be used to interact with your graphs, such as Selection, Drill Down, Zooming and Scrolling. There are two main events used to handle these functions, these being the Chart.MouseMove
and Chart.MouseDown
Event handlers. The use of these functions is very similar, this being to do a HitTest
on the mouse position, determining whether the mouse is over a DataPoint
, LegendItem
, Title
, etc., and then performing your actions based on where the user clicked.
Selection
In the demo app, if the user moves over any DataPoint
or LegendItem
, the points border turns white and thickens. When the user moves away, the point returns to normal.
Private Sub chSales_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) Handles mscSales.MouseMove
Dim htrResult As HitTestResult = mscSales.HitTest(e.X, e.Y)
For Each dp As DataPoint In mscSales.Series(0).Points
dp.BackSecondaryColor = Color.White
dp.BackHatchStyle = ChartHatchStyle.None
dp.BorderWidth = 0
Next dp
If htrResult.PointIndex >= 0 Then
If htrResult.ChartElementType = ChartElementType.DataPoint _
Or htrResult.ChartElementType = ChartElementType.LegendItem Then
Me.Cursor = Cursors.Hand
Dim dpSelected As DataPoint
dpSelected = mscSales.Series(0).Points(htrResult.PointIndex)
dpSelected.BackSecondaryColor = Color.Black
dpSelected.BorderColor = Color.White
dpSelected.BorderWidth = 1
End If
Else
Me.Cursor = Cursors.Default
End If
End Sub
Zooming
Zooming is achieved simply by enabling Zoomable on each axis of a series. This can be done in the designer, or it could be done at runtime.
Chart.ChartAreas(0).AxisX.ScaleView.Zoomable = True
Along with setting up zooming, there are various options to set, such as to determine the position of the scrollbars, once zoomed in.
Chart.ChartAreas(0).AxisX.ScrollBar.IsPositionedInside = True
Drill Down
Drilling down on a graph is achieved by determining which Data Point was selected by the user and making choices depending on the result. In the demo app, a user can select a specific Product Category, and depending on which Category is selected, a new Doughnut Graph will be displayed showing data for the products within that category.
Private Sub chSales_MouseDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) Handles mscSales.MouseDown
Dim htrResult As HitTestResult = mscSales.HitTest(e.X, e.Y)
If htrResult.ChartElementType = _
ChartElementType.DataPoint And htrResult.PointIndex >= 0 Then
SetGraphMonth()
If gsCurrentStage = GraphStage.Pyramid Then
gsCurrentStage = GraphStage.Doughnut
CurrentCategory = lstCategories(htrResult.PointIndex)
tbGroupPercentage.Value = 3
rbtnUnitSales.Visible = True
SetUpGraph()
Else
Dim newProductGraph As New ProductSales(CType_
(lstDataPoints(htrResult.PointIndex).Tag, Product))
newProductGraph.Show()
End If
End If
End Sub
Tool Tips
Tooltips can be set on various different aspects of the graph, such as Data Points, Legend Items, Titles, etc. These can be setup in the designer, or they can be set during Runtime, by simply detecting which area has been selected, and then setting the ToolTip. This is all done in the Chart.GetToolTip
event handler.
Private Sub chSales_GetToolTipText(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataVisualization.Charting.ToolTipEventArgs) _
Handles mscSales.GetToolTipText
Select Case e.HitTestResult.ChartElementType
Case ChartElementType.DataPoint
If e.HitTestResult.PointIndex >= 0 Then
Dim strKey As String = lstDataPoints_
(e.HitTestResult.PointIndex).DataKey
Dim strValue As String = Format(lstDataPoints_
(e.HitTestResult.PointIndex).DataValue, "#,##0.00")
e.Text = strKey & ", " & strValue
End If
End Select
End Sub
Printing
The Chart control uses a PrintManager
object to manage printing, which is encapsulated in the Chart.Printing
property. The PrintManager
has several methods that are used for very basic printing operations, such as PageSetup
, PrintPreview
and Print
. These are pretty self explanatory, although it should be said that the Print
method only prints a picture of the graph on your paper and nothing else. Although I played around with it a lot, I could not get the Print
method to print very well, if such things like borders, etc. were anything except very simple. I found the best way to print reports based on the graph was to set up a PrintPage
routine to handle printing, and add a Delegate
to handle the specific graphs PrintPage
event. In the PrintPage
routine, you can design your report as you like.
Private Sub PrintPage(ByVal sender As Object, ByVal ev As PrintPageEventArgs)
Dim titlePosition As New Rectangle(ev.MarginBounds.X, ev.MarginBounds.Y, _
ev.MarginBounds.Width, ev.MarginBounds.Height)
Dim stream As New System.IO.MemoryStream() CurrentGraph.SaveImage(stream, System.Drawing.Imaging.ImageFormat.Bmp) Dim bmp As New Bitmap(stream) Dim recPic As New Rectangle(ev.MarginBounds.X,
ev.MarginBounds.Y, bmp.Width, bmp.Height)
Dim fontTitle As New Font("Times New Roman", 16)
Dim chartTitle As String = String.Empty
If CurrentGraph.Titles.Count > 0 Then
chartTitle = CurrentGraph.Titles(0).Text
End If
Brushes.Black, titlePosition, format)
Dim chartPosition As New Rectangle(ev.MarginBounds.X, titlePosition.Bottom,
CurrentGraph.Size.Width, CurrentGraph.Size.Height)
Dim chartWidthScale As Single = 1 Dim chartHeightScale As Single = 1 chartPosition.Width = CInt(chartPosition.Width * _
Math.Min(chartWidthScale, chartHeightScale))
chartPosition.Height = CInt(chartPosition.Height * _
Math.Min(chartWidthScale, chartHeightScale))
ev.Graphics.DrawImage(bmp, recPic)
End Sub
As you can see, I played around with this section a lot, trying different things.
Saving Graphs to Disk as Images
Your graphs can be saved as various different types of image, such as bitmap, TIFF, JPG, PNG, etc. using the SaveImage
method of the Chart control. This method takes two parameters, the filename of the image to be saved, and a ChartImageFormat enum
specifying the type of image i.e., BMP, PNG, etc.
Conclusion
This Chart Control seems to be OK to handle most simple, and some quite complicated situations. The use of the control is very simple, and most of the properties and methods all seem pretty self-explanatory, and well named. To get a team up and running with this control would take no more than a few days, really, and the results are quite satisfactory, I think. Considering the expense of the alternatives, or the cost and time factor in building your own control, I would conclude that this is a fairly serious alternative.
History
- 16th March, 2010: Initial post