Milos has a simple graphing and charting engine. The idea of this graphing engine is not to compete with advanced graphing packages. Instead, the Milos SimpleGraphs engine has a few very focused goals:
Note: Web graphing has not yet been made available, although the utilized architecture allows us to run the engine both in Windows and in ASP.NET. We expect that this graphing engine will be improved over time, although it will probably not be a high priority until more advanced features are needed.
For use in Windows application, the Simple Graphs Engine provides a specialized Windows Forms control called "SimpleGraph". This control is defined in SimpleGraphs.dll. The control can be used by adding it to the Windows Forms toolbox. (To do so, right-click the toolbox, pick "Add/Remove Controls", and browse to the DLL).
Currently, the SimpleGraph control supports simple designer interaction, although much of the definition of the graph is done in source code. The following example shows how to create a simple graph with 4 individual data series, 2 of which are rendered as a curve ("spline"), and 2 are rendered as vertical bars. The following image shows the desired result:

To follow allong, create a new windows forms application (preferrably in C#, although the VB version is very similar), and drop the SimpleGraph control on the form. Keep its default name ("simpleGraph1"). You can then proceed to set properties on the control using the visual designer (and the design preview), or you can make changes in source code, as we will do it in this paper. All the code we are going to write in this example can be added to the form's constructor (Sub New in VB), just below the call to InitializeComponent(). Note however, that the same code go into practically any other method on the form.
As discussed, all the code we want to write goes into the constructor:
public Form1()
{
//
// Required
for Windows Form Designer support
//
InitializeComponent();
// Our code goes here!!!
}
Make sure the InitializeComponent() line occurs before the code we are about to write!
The first step we need to take is setting some properties that influence the overall appearance of the graph, such as the heading (Text) and axis labels:
this.simpleGraph1.Text = "Milos Sample Graph";
this.simpleGraph1.ChartArea.LabelY = "Number of
Hits";
this.simpleGraph1.ChartArea.LabelX = "Days
ago...";
Note that some properties are set on the control itself, but most useful properties (as we will see further below) are on other objects, such as the ChartArea. The ChartArea is an object that defines the rendering of the main chart area, such as the background grid, as well as labels on the x and y axis. The control is designed in a way that allows us to replace the chart area object with a different object, if we wanted an unusual rendering option. In most scenarios however, this will not be necessary.
The chart area will adjust to the data we are about to display within it.If we want to display very large numbers, the y-axis will automatically scale for instance. However, sometimes, the control will scale to somewhat odd numers, or decide to create an unusual number of grid lines and tick-marks. In our example, we want to display values that are roughly in the range between 0 and 100. We want a tick-mark and a label on the y-axis in increments of 10. This makes a total number of 11 labels and tick-marks. We can rely on the control to automatically come to the same conclusion, but this is somewhat unlikely, depending on the size of the control. We can also set the number of tick-marks (or "points") we want on the y-axis manually:
this.simpleGraph1.ChartArea.PointsY = 11;
Similarily, we can provide some guidance on what the values along the z-axis mean. Without any help, our control will not be able to make much sense out of the y-axis, other than considering the left-most point to be 0. In our simple example graph. we want to show the flow of data over the course of a few days. To create a simple indication of that, we can tell the control what the maximum expected value on the x-axis is:
this.simpleGraph1.ChartArea.MaxX = 7;
There are a few more properties that can be set that define the overal appearance of the graph, such as wether or not a legend should be displayed, and if so, where it should appear.
Now that we have some basic properties configured, we can proceed and add data to the control. Graph data is organized in "series", and each series has a number of individual data "points". Depending on the style of the series, this data can then be displayed in different ways, such as a line graph, or a bar chart.
The first series we want to add to the graph is a blue spline graph (a "curve"), with labels on the line (displaying the actual value), as well as little data points (circles in our case, although we could also use diamond shapes). Before we can add the first series however, it is always a good idea to make sure that there isn't any data in the graph already. In our case, this would be unlikely, but we should not rely on the graph to be empty. We can simply clear out the graph, by removing all data series:
this.simpleGraph1.Series.Clear();
Then, we can proceed to add a new series, immediately assign a label, and finally, we can display the style we want our series to take on:
this.simpleGraph1.Series.Add("Series 1");
this.simpleGraph1.Series[0].ShowDataLabels = true;
this.simpleGraph1.Series[0].PointStyle
=
EPS.Windows.Forms.SimpleGraphs.DataPointStyle.Point;
Note that blue is the default color for data series, so we do not have to set that. Also, the default style for a series is "spline", so we did not have to set that either.
All that's missing from this series now are the individual data points. We can simply use the points collection to add new values:
this.simpleGraph1.Series[0].Points.Add(80);
this.simpleGraph1.Series[0].Points.Add(10);
this.simpleGraph1.Series[0].Points.Add(70);
this.simpleGraph1.Series[0].Points.Add(40);
this.simpleGraph1.Series[0].Points.Add(20);
this.simpleGraph1.Series[0].Points.Add(70);
Note: In a real-life application, values would probably not be hardcoded. Instead, they would likely be retrieved from a data set, and be populated in a loop.
We can now add additional series in a similar fashion. The second series will be a red spline without any data points rendered into the graph (data points - such as little circles or diamonds - are turned off by default):
this.simpleGraph1.Series.Add("Second Series");
this.simpleGraph1.Series[1].LineColor = Color.Red;
this.simpleGraph1.Series[1].Points.Add(20);
this.simpleGraph1.Series[1].Points.Add(60);
this.simpleGraph1.Series[1].Points.Add(40);
this.simpleGraph1.Series[1].Points.Add(50);
this.simpleGraph1.Series[1].Points.Add(25);
this.simpleGraph1.Series[1].Points.Add(90);
One of the nice features of the Simple Graphs Engine is that different types of graphs can be mixed together. For instance, we can add a third data series that uses a vertical-bar style:
this.simpleGraph1.Series.Add("Final Data Series");
this.simpleGraph1.Series[2].ShowDataLabels = true;
this.simpleGraph1.Series[2].Style
=
EPS.Windows.Forms.SimpleGraphs.SeriesStyle.VerticalBars;
this.simpleGraph1.Series[2].LineColor =
Color.DarkGreen;
this.simpleGraph1.Series[2].FillColor =
Color.Olive;
this.simpleGraph1.Series[2].Points.Add(40);
this.simpleGraph1.Series[2].Points.Add(5);
this.simpleGraph1.Series[2].Points.Add(22);
this.simpleGraph1.Series[2].Points.Add(85);
this.simpleGraph1.Series[2].Points.Add(60);
this.simpleGraph1.Series[2].Points.Add(95);
It is important to note that the series style is set explicitly. Also, bar charts allow the developer to specify different colors for the inside of the bar and the outline of the bar, a feature which we are taking advantage of here. Note also, that data labels are turned on for this series, resulting in values being displayed above each bar (although no data points - such as circles or diamonds - are supported by bar charts).
Finally, we can add a fourth series (see code below), which will be rendered as yet another bar graph. The complete code for the constructor of the form should be similar to the following example:
public Form1()
{
//
// Required
for Windows Form Designer support
//
InitializeComponent();
this.simpleGraph1.Text = "Milos Sample
Graph";
this.simpleGraph1.ChartArea.LabelY = "Number of Hits";
this.simpleGraph1.ChartArea.LabelX = "Days ago...";
this.simpleGraph1.ChartArea.MaxX = 7;
this.simpleGraph1.ChartArea.PointsY =
11;
this.simpleGraph1.Series.Clear();
this.simpleGraph1.Series.Add("Series 1");
this.simpleGraph1.Series[0].ShowDataLabels = true;
this.simpleGraph1.Series[0].PointStyle
=
EPS.Windows.Forms.SimpleGraphs.DataPointStyle.Point;
this.simpleGraph1.Series[0].Points.Add(80);
this.simpleGraph1.Series[0].Points.Add(10);
this.simpleGraph1.Series[0].Points.Add(70);
this.simpleGraph1.Series[0].Points.Add(40);
this.simpleGraph1.Series[0].Points.Add(20);
this.simpleGraph1.Series[0].Points.Add(70);
this.simpleGraph1.Series.Add("Second Series");
this.simpleGraph1.Series[1].LineColor = Color.Red;
this.simpleGraph1.Series[1].Points.Add(20);
this.simpleGraph1.Series[1].Points.Add(60);
this.simpleGraph1.Series[1].Points.Add(40);
this.simpleGraph1.Series[1].Points.Add(50);
this.simpleGraph1.Series[1].Points.Add(25);
this.simpleGraph1.Series[1].Points.Add(90);
this.simpleGraph1.Series.Add("Final Data Series");
this.simpleGraph1.Series[2].ShowDataLabels = true;
this.simpleGraph1.Series[2].Style =
EPS.Windows.Forms.SimpleGraphs.SeriesStyle.VerticalBars;
this.simpleGraph1.Series[2].LineColor =
Color.DarkGreen;
this.simpleGraph1.Series[2].FillColor =
Color.Olive;
this.simpleGraph1.Series[2].Points.Add(40);
this.simpleGraph1.Series[2].Points.Add(5);
this.simpleGraph1.Series[2].Points.Add(22);
this.simpleGraph1.Series[2].Points.Add(85);
this.simpleGraph1.Series[2].Points.Add(60);
this.simpleGraph1.Series[2].Points.Add(95);
this.simpleGraph1.Series.Add("Nahh.... one more");
this.simpleGraph1.Series[3].Style =
EPS.Windows.Forms.SimpleGraphs.SeriesStyle.VerticalBars;
this.simpleGraph1.Series[3].LineColor =
Color.Goldenrod;
this.simpleGraph1.Series[3].FillColor =
Color.Yellow;
this.simpleGraph1.Series[3].Points.Add(30);
this.simpleGraph1.Series[3].Points.Add(25);
this.simpleGraph1.Series[3].Points.Add(50);
this.simpleGraph1.Series[3].Points.Add(30);
this.simpleGraph1.Series[3].Points.Add(55);
this.simpleGraph1.Series[3].Points.Add(50);
}
The result of this is displayed in the above screen shot.
The Simple Graphs Engine is designed to be enhanced with additional series rendering styles. This can be done relatively easily, although recompilation is required. Initially, the graph control only supported Spline and Vertical Bar styles for data series. In this paper, we will develop another style called "Area", which will fill an area with a defined color. This rendering style has now been added to the Simple Graphs engine.
The first step towards enhancing the rendering mechanism is to modify the enum of available styles. This enum is called SeriesStyle and can be found in DataSeriesCollection.cs. For our example, we will add the Area type:
///
///
public enum SeriesStyle
{
VerticalBars,
Splines,
Area
}
Note: By the time you read this, this enum may have grown much larger.
Creating this enum member allows us to set the "Area" style on a data series. However, there is currently now class that knows how to render Area style. We can fix this problem by creating such a class. The general idea behind rendering classes is that they provide methods that can render the desired output through static methods that can be called without the object having to be instantiated. (This is only a suggested design that can be altered if needed). Here is the class definition:
using
System;Note that the class is added to the appropriate namespace. Also the constructor of the class is made private, so it can not be instantiated. This is what we desire, since all the members on the class will be defined as static, which means that the class does not have to be instantiated. The definition also includes GDI+ namespaces, so we can use GDI+ objects conveniently.
The next step now is to add a method to this class that can render the Area. The method needs to accept 3 parameters:
///
///
/// Graphics
object
/// Chart
area
/// Data
Series
public static void Render(Graphics g, IChartArea ChartDrawingArea,
DataSeries Series)
{}
Using the Series object, we can create a definition of all our data points (or their visual on-screen representation). In our example, we will have to create a graphics path that defines the outline of the area we want to draw on the screen. The easiest step towards that is to first create an array of points, containing exactly as many points as we have in the data series, plus two more since we want to fill an area that reaches all the way to the bottom line of the chart, which requires two more corner points:
// To render an
area, we will need an array of points
Point[] aPoints = new Point[Series.Points.Count+2];
The most difficult step in rendering our area-chart is finding out where those points are to be positioned on the screen. To start out with, we know the default horizontal (X) position based on the chart area object that is passed to us:
int
iCurrentX = ChartDrawingArea.ChartDataRectangle.X;Our data points will then be alligned sequentually along the x-axis. However, whenever a chart flows from left-to-right, there is extra space added to the left and the right of the data area. We can test whether this is the case using the following code:
if
(ChartDrawingArea.DataDirection == DataAxisDirection.X)
{
// We can not use the first point
and need to start with the second point instead
iCurrentX += (int)ChartDrawingArea.SpaceBetweenXPoints;
}
Don't worry about the details of the data axis direction. Just make sure you always add this if-statement.
Note that this code makes use of one very important piece of information: The space between individual data points. We can get that information through the SpaceBetwwenXPoints property on the chart drawing area. Voila! This is basically all we need. We can now use this information to calculate the position of every single point within our area simply by iterating over all data points and by performing some spacial calculations. Don't worry about the details too much, as they are very similar in every implementation. here is that code:
// We now need to fill
the points array with the appropriate data
int iCounter =
0;
foreach (DataPoint oPoint in
Series.Points)
{
aPoints[iCounter].X = iCurrentX;
int iY = (int)((ChartDrawingArea.ChartDataRectangle.Height/ChartDrawingArea.MaxY)*oPoint.Value);
iY =
(ChartDrawingArea.ChartDataRectangle.Height +
ChartDrawingArea.ChartDataRectangle.Top) - iY;
aPoints[iCounter].Y =
iY;
iCounter++;
iCurrentX += (int)ChartDrawingArea.SpaceBetweenXPoints;
}
In addition, we need to define the bottom right and bottom left corner of the filled area. We can do this by using the bottom of the available drawing area as well as the last and first x-coordinates of the actual data points:
aPoints[aPoints.Length-2].X =
aPoints[aPoints.Length-3].X;
aPoints[aPoints.Length-2].Y =
ChartDrawingArea.ChartDataRectangle.Bottom;
aPoints[aPoints.Length-1].X =
aPoints[0].X;
aPoints[aPoints.Length-1].Y =
ChartDrawingArea.ChartDataRectangle.Bottom;
Now that we have all the points that make up the top line of our area, we can use these points to define a graphics path:
// We can define
the graphics path required to do the rendering
GraphicsPath oPath =
new
GraphicsPath();
oPath.AddPolygon(aPoints);
Voila! Now all that is left to do is using two simple method calls that will fill the defined area and draw the outline:
// We can now
render the path
g.FillPath(new
SolidBrush(Series.FillColor),oPath);
g.DrawPath(new
Pen(Series.LineColor,2),oPath);
oPath.Dispose();
Note that we use the defined fill and line colors for the series. Once we are done, we dispose of the path, since we do not neet it anymore, and we do not want it to use up system resources.
At this point, we have a new renderer that can produce an area graph. However, this renderer is not yet invoked. We have to add the code that calls our new renderer manually. This is done on Render() method the DataSeries class. All you need to do is change the switch() statement to call the new render code when the series style is "Area". Once that modification is made, the method should look similar to this (only the bold lines need to be added):
///
///
///
Graphics object
///
Chart area
public void Render(Graphics g, IChartArea
ChartDrawingArea)
{
// First, we need to find out what renderer to
use
switch (this.Style)
{
case
SeriesStyle.Splines:
SplineDataRenderer.Render(g,ChartDrawingArea,this);
break;
case
SeriesStyle.VerticalBars:
BarRenderer.Render(g,ChartDrawingArea,this);
break;
case
SeriesStyle.Area:
AreaDataRenderer.Render(g,ChartDrawingArea,this);
break;
}
}
When you run the chart control and add a data series with its style set to "Area", the result should look similar to this:

At this point, our chart does not support series points and labels. We should add this functionality by checking the ShowDataLabels property, and if that is set to true, render simple labels for each true point, and perhaps also a little visual data point (such as a circle or a diamond). The result will be similar to the graph shown in the following screen shot:

The following is the entire code snipped needed for the Render() method to render the actual areas, as well as the labels:
///
///
/// Graphics object
///
Chart area
///
Data Series
public static void Render(Graphics g,
IChartArea ChartDrawingArea, DataSeries Series)
{
// To render an area, we will need
an array of points
Point[] aPoints = new
Point[Series.Points.Count+2];
// What is the appropriate
x-position
int iCurrentX =
ChartDrawingArea.ChartDataRectangle.X;
if
(ChartDrawingArea.DataDirection == DataAxisDirection.X)
{
// We can not use the first point
and need to start with the second point
instead
iCurrentX += (int)ChartDrawingArea.SpaceBetweenXPoints;
}
// We now need to fill the points
array with the appropriate data
int iCounter =
0;
foreach (DataPoint oPoint in Series.Points)
{
aPoints[iCounter].X =
iCurrentX;
int iY = (int)((ChartDrawingArea.ChartDataRectangle.Height/ChartDrawingArea.MaxY)*oPoint.Value);
iY =
(ChartDrawingArea.ChartDataRectangle.Height +
ChartDrawingArea.ChartDataRectangle.Top) -
iY;
aPoints[iCounter].Y =
iY;
iCounter++;
iCurrentX += (int)ChartDrawingArea.SpaceBetweenXPoints;
}
aPoints[aPoints.Length-2].X =
aPoints[aPoints.Length-3].X;
aPoints[aPoints.Length-2].Y =
ChartDrawingArea.ChartDataRectangle.Bottom;
aPoints[aPoints.Length-1].X =
aPoints[0].X;
aPoints[aPoints.Length-1].Y =
ChartDrawingArea.ChartDataRectangle.Bottom;
// We can define the
graphics path required to do the rendering
GraphicsPath oPath = new GraphicsPath();
oPath.AddPolygon(aPoints);
// We can now render
the area
g.FillPath(new
SolidBrush(Series.FillColor),oPath);
g.DrawPath(new
Pen(Series.LineColor,2),oPath);
oPath.Dispose();
// If
we need to display point labels, we will now create those as
well
if
(Series.ShowDataLabels)
{
if
(Series.PointStyle !=
DataPointStyle.None)
{
// First, we need a
path that represents the label point style
GraphicsPath pathPoint =
new
GraphicsPath();
switch
(Series.PointStyle)
{
case
DataPointStyle.Diamond:
pathPoint.AddLine(0,-6,5,0);
pathPoint.AddLine(0,6,-5,0);
break;
default: // Including
"Point"
pathPoint.AddEllipse(-4,-4,8,8);
break;
}
// We can now render
all the points wherever we need them
SolidBrush brshPoint = new
SolidBrush(Series.LineColor);
for
(int i=0;
i
g.TranslateTransform(aPoints[i].X,aPoints[i].Y);
g.FillPath(brshPoint,pathPoint);
g.ResetTransform();
}
brshPoint.Dispose();
}
// We now also render the series
labels
StringFormat sfPoint = new
StringFormat();
sfPoint.Alignment =
StringAlignment.Center;
Rectangle rectPointLabel = new
Rectangle(-100,0,200,20);
for (int i=0;
i
g.TranslateTransform(aPoints[i].X,aPoints[i].Y-20);
g.DrawString(Series.Points[i].Value.ToString(),
Series.oDataPointFont,Brushes.Black,rectPointLabel,sfPoint);
g.ResetTransform();
}
sfPoint.Dispose();
}
}
At this point, our Area renderer is almost complete. The only minor problem is that there is not entry in the legend for this chart type. We also need to add some rendering code for that purpose to our class. To do so, we create a second static method that accepts 3 parameters: A graphics object, a suggested rendering rectangle, and a reference to the data series. Using this information, we can draw a simple label, as well as a graphical icon representing the chart, allowing the user to identify the data on the screen.
The most difficult part here is to figure out where to draw. However, this is made simple, since the second parameter passed to the method is a suggested rendering rectangle. Armed with that information, we can figure out where to render the label. In addition, we create a simple arbitrary graphics path that represents an area chart, and render that into the suggested rectangle as well. Here is the code that performs that operation:
///
///
///
Graphics object
///
Suggested drawing rectangle
///
Data series
public static void
RenderLegend(Graphics g, Rectangle SuggestedTarget, DataSeries
Series)
{
// We render the
visual representation of the graph element using a dummy
path
GraphicsPath oPath = new
GraphicsPath();
oPath.AddLine(SuggestedTarget.Left,SuggestedTarget.Bottom-4,SuggestedTarget.Left+10,SuggestedTarget.Top+5);
oPath.AddLine(SuggestedTarget.Left+15,SuggestedTarget.Top+10,SuggestedTarget.Left+26,SuggestedTarget.Top+1);
oPath.AddLine(SuggestedTarget.Left+26,SuggestedTarget.Bottom-4,SuggestedTarget.Left,SuggestedTarget.Bottom-4);
g.FillPath(new
SolidBrush(Series.FillColor),oPath);
g.DrawPath(new
Pen(Series.LineColor),oPath);
oPath.Dispose();
// We render the
label
Rectangle rectLabel = new
Rectangle(SuggestedTarget.X + 30,
SuggestedTarget.Y,SuggestedTarget.Width-30,SuggestedTarget.Height-2);
StringFormat sf = new StringFormat();
sf.LineAlignment =
StringAlignment.Center;
g.DrawString(Series.Text,new
Font("Tahoma",8),Brushes.Black,rectLabel,sf);
sf.Dispose();
}
Note that most of this code is very similar in every renderer. This is especially true for the second half, which renders the label.
This code is not yet being called. However, we can easily invoke this code whenever a series is supposed to render its legend, by adding another case to the switch() statement in the Render:Legend() method of the DataSeries class. Once again, only the bold lines need to be added:
///
///
///
Graphics object
///
Suggested legend drawing space
public void RenderLegend(Graphics g, Rectangle
TargetRectangle)
{
// First, we need to find out what renderer to
use
switch (this.Style)
{
case
SeriesStyle.Splines:
SplineDataRenderer.RenderLegend(g,TargetRectangle,this);
break;
case
SeriesStyle.VerticalBars:
BarRenderer.RenderLegend(g,TargetRectangle,this);
break;
case
SeriesStyle.Area:
AreaDataRenderer.RenderLegend(g,TargetRectangle,this);
break;
}
}
That's it! Our new series style is now completely functional. Here is a screenshot that shows the very first example of this paper, with the second series changed to Area-style (note the second entry in the legend):
