Aaron Goldenthal

Sometimes ASP.NET is Rocket Science

Creating a Gantt Chart with the MS Chart Controls

A recent project had me diving head first into creating charts in ASP.NET.  Hoping to find a free alternative, I turned to the Microsoft Chart Controls which provides a powerful set of tools to create a variety of chart types.  One of the required charts was a simple Gantt chart, which we’ll be creating here using a stacked bar chart. This article assumes you have a working level knowledge of using the Microsoft Chart Controls.  If you need help with the basics, there’s a good set of articles from Scott Mitchell here.   Using the Stacked Bar Chart A stacked bar chart, as the name implies, stacks different series of data on top of each other in a horizontal bar.  The image below shows an example from the Chart Controls samples download. In the example above, there are four Series and each makes up part of each bar (light blue, orange, red, and dark blue).  When points are added to each Series, an X and one Y value are provided (as opposed to some chart types, e.g. the range chart, which can take multiple Y values).  The X values in the various series are used to denote related data, i.e. data with the same X values are stacked on top of each other.  The axes for this chart are different from what you might expect from most other charts - the vertical axis represents the various X values, and the Y values are plotted along the horizontal axis. One important point to note is that the Y values are not the actual position of the end of that segment of the bar, instead they’re the width of that segment of the bar.  In the example above, the values for the four series for bar #1 are 48, 5, 70, 70 - they are not 48, 53, 123, 193.  Since only magnitudes are provided for the width of each bar segment, and not start/end points, all of the bars have a common starting point – a Y value of zero.  We’ll see later what this all means for our Gantt chart, where the Y axis is a series of dates. Creating the Chart For this example, we’ll work with a couple of simple project objects with a few properties – Name, Start, End, and PercentComplete.  1: var project1 = new { Name = "Project 1", Start = new DateTime(2010, 03, 01), 2: End = new DateTime(2010, 06, 01), PercentComplete = 75 }; 3: var project2 = new { Name = "Project 2", Start = new DateTime(2010, 02, 01), 4: End = new DateTime(2010, 07, 01), PercentComplete = 50 }; To start with, we’ll create a simple Gantt chart to show the overall project durations. 1: <asp:Chart ID="Chart1" runat="server" Width="500" Height="125"> 2: <Series> 3: <asp:Series Name="StartSeries" ChartType="StackedBar" 4: Color="White"></asp:Series> 5: <asp:Series Name="ProjectDurationSeries" ChartType="StackedBar" 6: Color="Green" BorderColor="Black" BorderWidth="2"></asp:Series> 7: </Series> 8: <ChartAreas> 9: <asp:ChartArea Name="ChartArea1"> 10: <AxisY> 11: <MajorGrid Enabled="false" /> 12: <LabelStyle Format="MM/yyyy" /> 13: </AxisY> 14: <AxisX> 15: <MajorGrid Enabled="false" /> 16: </AxisX> 17: </asp:ChartArea> 18: </ChartAreas> 19: </asp:Chart> Two Series are defined for this Chart.  As was previously noted, all bars in a stacked bar chart will have the same starting point.  The projects we’re using, however, do not have the same start date.  To work around this, the first Series (StartSeries) will simply be a placeholder so the project duration bars will start at the appropriate points.  Its color is set to the same as the background so that it is not visible.  The second Series (ProjectDurationSeries) will show the overall project duration, and is given some simple formatting (a green bar with a black 1 pixel border). There’s also some minimal formatting of the ChartArea: both the X and Y grids are disabled, and the date format is set to show month/year.  As noted before, the horizontal axis is the Y axis, not the X axis (hence the date formal is applied to the LabelStyle of the Y axis). To plot the data, we’ll add a point to each Series for each project. 1: Chart1.Series["StartSeries"].Points.AddXY(project1.Name, project1.Start); 2: Chart1.Series["StartSeries"].Points.AddXY(project2.Name, project2.Start); 3: Chart1.Series["ProjectDurationSeries"].Points.AddXY(project1.Name, project1.End); 4: Chart1.Series["ProjectDurationSeries"].Points.AddXY(project2.Name, project2.End); 5: Chart1.DataBind(); The project names are used as the X values to relate the data for the two series, and these will end up as the labels on the X axis (the vertical axis).  The Y values for StartSeries are the start dates for each project so that the duration bars start at the correct location.  For the ProjectDurationSeries, we’ll simply use the project end dates as the Y values (mostly to illustrate why this isn’t correct). The resulting Chart is shown below. From this we can see a few things.  As noted before, the Y values (in this case the Start date) are plotted along the horizontal axis.  For each point we’ve added to the Series, a separate bar is created, with the vertical axis labels being the X values we provided – in this case the project name.  We can also see that this Chart is is not quite what we’re looking for.  Let’s examine how the Charts control is processing the data we’re providing to see why. When a DateTime is passed to the AddXY method, a couple of things happen.  First, the object type is used to set the XValueType/YValueType for the Series.  In this case for the Y values, that’s ChartValueType.DateTime.  The Chart control ultimately plots numbers, including DateTimes, as doubles.  To convert the DateTime to a double the DateTime.ToOADate method is used.  This method returns a double that is the equivalent OLE Automation date, which represents the date as a floating point number with the integral values as days and an origin of midnight on December 29, 1899 (which is the origin of the Y axis in the above Chart – a the OLE Automation date of zero). Unfortunately, this means we’re not really plotting what we want.  As an example, for Project 1, the Y value for StartSeries is 40238, which is the correct start date.  Because of the way the StackedBar chart is rendered, what we want for the ProjectDurationSeries Y value is the difference between the start and end dates.  What we’re actually getting is the end date converted to a double, which is 40330, the difference between the end date and the origin of an OLE Automation date, which is why the bar extends to August 1, 2120 (the OLE Automation date for 40238 + 40330). To solve this problem, we’ll make a few changes to the code. 1: Chart1.Series["StartSeries"].Points.AddXY(project1.Name, project1.Start); 2: Chart1.Series["StartSeries"].Points.AddXY(project2.Name, project2.Start); 3: Chart1.Series["ProjectDurationSeries"].Points.AddXY(project1.Name, 4: (project1.End - project1.Start).TotalDays); 5: Chart1.Series["ProjectDurationSeries"].Points.AddXY(project2.Name, 6: (project2.End - project2.Start).TotalDays); 7: Chart1.ChartAreas[0].AxisY.Minimum = (new DateTime(2010, 1, 1)).ToOADate(); 8: Chart1.ChartAreas[0].AxisY.Maximum = (new DateTime(2010, 8, 1)).ToOADate(); 9: Chart1.DataBind(); Since the values for StartSeries were correct, the first change is the data we’re adding to the ProjectDurationSeries.  Instead of simply passing the End date, we’ll calculate the difference between the Start and End dates and convert that to a double through the TotalDays property of the resulting TimeSpan.  Since this uses the same time scale as an OLE Automation date (one day = 1), and all we’re looking for is a duration for the project, this will render correctly in the chart. We could render the Chart with only that change and it would be correct, but the min/max values would be the same as what they were before (from 1899 to 2200), so we’ve explicitly set the Minimum and Maximum values for the Y Axis to give a one month margin on either side of our min/max values from the two projects.  The Minimum and Maximum properties are doubles, so we’ll use the same process that the Chart uses to convert DateTimes to doubles and call ToOADate. The result is the plot below, which now correctly represents the duration of each project. To make this plot a little more useful, we’re going to make a few more changes and finally use the PercentComplete property of each project.   Instead of simply plotting one bar for the overall project duration, we’ll split that up into the complete and incomplete work on the project. First, we’ll make a few changes to the Chart markup. 1: <asp:Chart ID="Chart1" runat="server" Width="500" Height="125"> 2: <Series> 3: <asp:Series Name="StartSeries" ChartType="StackedBar" 4: Color="White"></asp:Series> 5: <asp:Series Name="ProjectCompleteSeries" ChartType="StackedBar" 6: Color="Green" BorderColor="Black" BorderWidth="2"></asp:Series> 7: <asp:Series Name="ProjectRemainingSeries" ChartType="StackedBar" 8: Color="LightYellow" BorderColor="Black" BorderWidth="2" 9: BackHatchStyle="ForwardDiagonal" BackSecondaryColor="Black"></asp:Series> 10: </Series> 11: <ChartAreas> 12: <asp:ChartArea Name="ChartArea1"> 13: <AxisY> 14: <MajorGrid Enabled="false" /> 15: <LabelStyle Format="MM/yyyy" /> 16: </AxisY> 17: <AxisX> 18: <MajorGrid Enabled="false" /> 19: </AxisX> 20: </asp:ChartArea> 21: </ChartAreas> 22: </asp:Chart> Since the second series is no longer plotting the full project duration, we’ve changed the name to ProjectCompleteSeries.  We’ve also added a third series, ProjectRemainingSeries, and set a few formatting properties. Finally, we’ll make a few changes to the code. 1: Chart1.Series["StartSeries"].Points.AddXY(project1.Name, project1.Start); 2: Chart1.Series["StartSeries"].Points.AddXY(project2.Name, project2.Start); 3:  4: double totalDays = (project1.End - project1.Start).TotalDays; 5: double completeDays = (project1.PercentComplete / 100f) * totalDays; 6: Chart1.Series["ProjectCompleteSeries"].Points.AddXY(project1.Name, 7: completeDays); 8: Chart1.Series["ProjectRemainingSeries"].Points.AddXY(project1.Name, 9: totalDays - completeDays); 10:  11: totalDays = (project2.End - project1.Start).TotalDays; 12: completeDays = (project2.PercentComplete / 100f) * totalDays; 13: Chart1.Series["ProjectCompleteSeries"].Points.AddXY(project2.Name, 14: completeDays); 15: Chart1.Series["ProjectRemainingSeries"].Points.AddXY(project2.Name, 16: totalDays - completeDays); 17:  18: Chart1.ChartAreas[0].AxisY.Minimum = (new DateTime(2010, 1, 1)).ToOADate(); 19: Chart1.ChartAreas[0].AxisY.Maximum = (new DateTime(2010, 8, 1)).ToOADate(); 20: Chart1.DataBind(); 21:  The Y values we’ll add for the ProjectCompleteSeries needs to represent the duration of the completed work.  To calculate that, first we’ll calculate the total duration of the project in days, then calculate the completed days using PercentComplete.  For the ProjectRemainingSeries, the Y values are simply the difference between the total and completed days. The result is the plot below. There’s obviously a lot more that could be done with this Chart, but hopefully enough of the basics have been covered to get started.