Tutorial: PowerShell and Microsoft Chart Controls (or How To Spice Up Your Reports)

As admins, we are used to digging through heaps of monochrome, barely formatted data, searching, selecting, sorting, filtering, until we got what we were looking for. That’s our job, that’s how we think, that’s what we lo…I get carried away. :)

My point is, while you might appreciate nicely presented, simplified data, you don’t consider it vital. So why would anybody else? The short answer is: what the technician considers ‘interesting information’, to the manager (and other uninitiated folk), it’s like Vogon poetry. They usually get bored and annoyed by technical details and things they don’t understand, which covers basically 99% of what you do. They like it simple and colorful. And they do love charts.

Microsoft Chart Controls

Microsoft Chart Controls (MCCs) is a great tool to spice up reports created by your monitoring script or to make your weekly log file analysis ‘management compatible’. You can create a wide range of different chart types (see “Chart Types” gallery above) , with almost complete freedom of design over the whole thing. And when you’re done, you can display your chart in a GUI (e.g. Windows Forms) or save it as a graphics file to include it in a HTML report or email.

The Basics

MCCs provide you with a huge set of classes to create your chart. And while they all offer interesting possibilities, only a few are really important at the beginning. A standard chart created with MCCs basically consists of three essential elements: a Chart object, a ChartArea object and one or more data Series.

To understand how they fit together, it’s easiest to visualize them as layers, which sit on top of each other:

Layers_s2

The Chart object is the first object you create. All other elements get attached to this object. It has a lot of properties which are only useful when you want to display your chart in a GUI. But it also determines the size of your chart(s).
The ChartArea object defines how the chart grid looks like, it also holds the axis titles and some overall design options (more about that later).
The data Series makes your chart a chart. It not only defines the displayed values (data points), but it also sets the style (“type”) your data is displayed in.

KeyElements_s
Although the whole thing has it’s logic, some settings (properties) are located where you wouldn’t expect them to be.
For example, nearly all of the style settings (which define how your data is presented) are set via the Series’ properties. So you probably would expect that if you want the chart to appear in a 3D style, this would be set there as well. But because this setting affects all Series in a ChartArea, you need to set the 3D style via the ChartArea object.
I tell you this simply because, if at some point you find yourself wondering why a certain detail of your chart apparently can’t be configured, you might just be looking in the wrong place. TwoChartAreas_OneChartObj

In general, a Chart object can hold multiple ChartAreas and a ChartArea can hold multiple data Series. (unless you use certain chart (style-) types which can only display one data series. Check the chart type gallery above for examples, e.g. pie, funnel, pyramid etc.)

There is one other object displayed in the illustration above, the Legend object. It defines (you guessed it) how the charts legend will look like. It’s optional, but useful if you have more than one data Series in your chart.

Alright, enough theory, let’s make an example:

Get Started

If you have the .NET framework v4.x installed on your pc you already have all you need.
For .NET v3.5 Sp1 you can install MCCs as an ‘add-on’. You can get the installation package here.

Example: Memory Usage Chart

For this tutorial we will create a chart that shows your pc’s top 5 processes by memory usage (private memory and virtual memory) and save it as a *.png-file.

We start by loading the necessary assembly and determine the script’s home path, because that’s where we save the chart-image to.

[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
$scriptpath = Split-Path -parent $MyInvocation.MyCommand.Definition

Now we create the Chart object.

As we won’t display this one in a GUI, we only need to set a few properties here: size and backcolor. (If you would however want to display in in a GUI, you simply need to set the same properties and follow the same procedures here as with all WinForms elements (tutorial).)

# chart object
   $chart1 = New-object System.Windows.Forms.DataVisualization.Charting.Chart
   $chart1.Width = 600
   $chart1.Height = 600
   $chart1.BackColor = [System.Drawing.Color]::White

BTW, when i say “chart“, i mean the whole diagram we’re creating here and when i say “Chart object” i mean the instance of the .NET-object we’ve just created and named “$chart1”.

Then we add a title for the chart. A Chart object can have multiple titles (e.g. a title and a subtitle, or a title for each ChartArea attached to this Chart object), so we create it by adding it directly to it’s title collection (called “Titles“).
And because this the first title in the collection, we refer to it like we do to any collection’s first element ([0]), when we set the properties for font and alignment.

# title 
   [void]$chart1.Titles.Add("Top 5 - Memory Usage (as: Column)")
   $chart1.Titles[0].Font = "Arial,13pt"
   $chart1.Titles[0].Alignment = "topLeft"

Next in line is the ChartArea.

As mentioned before, a Chart object can have multiple ChartArea objects. Per default, they will automatically share the available space of the Chart object (determined by the Chart object’s height and width properties). But you can also customize the location and size of each ChartArea by setting the “$ChartArea.Position.Auto” – property to $false and set size and position yourself (see also here).

For now, we are happy with the default behavior, so we just set the titles for the X Empty_ChartAreaand Y axis of the chart grid and as well as the axis’ interval. The interval should be set according to the data you will display there. On the X-axis we’ll display only the five processes, so we want to have a grid line for every process. On the Y-axis, the memory values (in MB) will be displayed. They can be quite high, so we draw a line only for every 100 MB.2_BorderDashPlusGradient3_Area3D

You can also customize a lot of other things here, e.g. if you want to have your chart in a 3D style or apply shadows, you need to set this here as well.

# chart area 
   $chartarea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
   $chartarea.Name = "ChartArea1"
   $chartarea.AxisY.Title = "Memory (MB)"
   $chartarea.AxisX.Title = "Process Name"
   $chartarea.AxisY.Interval = 100
   $chartarea.AxisX.Interval = 1
   $chart1.ChartAreas.Add($chartarea)

We’ll also need a Legend object, because we’ll have two data Series in the same ChartArea and it’s probably a good idea to be able to tell which is which. :)
Like the Title(s), a Chart object can have multiple legends, so it’s a collection our legend gets added to and like with the ChartArea, you could override the auto-placement and sizing, if you want to.
There are a couple of other design options, just try them out.

# legend 
   $legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
   $legend.name = "Legend1"
   $chart1.Legends.Add($legend)

You might wonder why we don’t configure the actual contents (color and text) of the legend here. That’s because they get automatically added when we assign the data Series’ to this legend.

To be able to fill our chart with the desired content, we need a data source. So we just use get-process, sort by private memory size and take the top five entries of the list.

# data source
   $datasource = Get-Process | sort PrivateMemorySize -Descending  | Select-Object -First 5

Ok, now before we can add a data Series, we need to talk about DataPoints for a moment.

Every value displayed in your chart is represented by a DataPoint. And every data point is a member of a DataPointCollection. And finally the DataPointCollection is attached to a data Series, which defines how and where the data is displayed.

Usually a data point displayed in a chart has two values: one X- and one Y-value. But in some chart types, there can be up to six Y-values for each DataPoint. Why? Take a look at the illustration:

Datapoints2c

To display a range-type chart for example, you need to be able to set where the range should start and where it ends. So you need two Y-values. Here is a good overview over the chart types and their particularities. The X- and Y-values of a DataPoint can’t be empty, if one is, it’s automatically set to zero instead.

On the X-axis, you can also display a string instead of a numeric value (like we want to display the process names there). But the “.XValue” – property of a DataPoint has to be numerical. So to achieve that you need to assign the string to the “.AxisLabel” – property of the DataPoint instead.

Now we add our first data Series.

We create it by setting a name for the Series and adding it directly to the chart’s Series collection. Then we set the chart type to column. When choosing the type, there is one important thing to consider: You know already that a ChartArea can have multiple Series. You can even combine different types in one ChartArea (you can combine a line chart with a point chart for example), but some types aren’t compatible with each other (here you can see what types can be combined).
You can decide if a Series is displayed in the charts Legend by setting the “.IsVisibleInLegend” – property and assigning the Legend’s name to the “.Legend” – property.
You also need specify the name of the ChartArea in which this Series should be displayed.

For the DataPoints, instead of creating them one by one, you can feed a data source via the pipeline to the “.AddXY()” – method of the Series’ DataPointCollection “.Points”, like we do here. (there are also a few other options, but that’s for another tutorial)
This way, we don’t even have to care that one value is numerical and the other a string. They get added automatically to the right property.

# data series
   [void]$chart1.Series.Add("VirtualMem")
   $chart1.Series["VirtualMem"].ChartType = "Column"
   $chart1.Series["VirtualMem"].BorderWidth  = 3
   $chart1.Series["VirtualMem"].IsVisibleInLegend = $true
   $chart1.Series["VirtualMem"].chartarea = "ChartArea1"
   $chart1.Series["VirtualMem"].Legend = "Legend1"
   $chart1.Series["VirtualMem"].color = "#62B5CC"
   $datasource | ForEach-Object {$chart1.Series["VirtualMem"].Points.addxy( $_.Name , ($_.VirtualMemorySize / 1000000)) }

Adding the second Series works exactly the same, only this time we feed the value of PrivateMemorySize:

# data series
   [void]$chart1.Series.Add("PrivateMem")
   $chart1.Series["PrivateMem"].ChartType = "Column"
   $chart1.Series["PrivateMem"].IsVisibleInLegend = $true
   $chart1.Series["PrivateMem"].BorderWidth  = 3
   $chart1.Series["PrivateMem"].chartarea = "ChartArea1"
   $chart1.Series["PrivateMem"].Legend = "Legend1"
   $chart1.Series["PrivateMem"].color = "#E3B64C"
   $datasource | ForEach-Object {$chart1.Series["PrivateMem"].Points.addxy( $_.Name , ($_.PrivateMemorySize / 1000000)) }

Finally, we want to save our chart to a image file. For that we use the “.saveImage()”-method of the Chart object. You can choose from various file formats.

# save chart
   $chart1.SaveImage("$scriptpath\SplineArea.png","png")

If you now run the script and open the png-file, your chart should look something like this:

SplineArea

And here’s the whole script:

[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
$scriptpath = Split-Path -parent $MyInvocation.MyCommand.Definition
 
# chart object
   $chart1 = New-object System.Windows.Forms.DataVisualization.Charting.Chart
   $chart1.Width = 600
   $chart1.Height = 600
   $chart1.BackColor = [System.Drawing.Color]::White
 
# title 
   [void]$chart1.Titles.Add("Top 5 - Memory Usage (as: Column)")
   $chart1.Titles[0].Font = "Arial,13pt"
   $chart1.Titles[0].Alignment = "topLeft"
 
# chart area 
   $chartarea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
   $chartarea.Name = "ChartArea1"
   $chartarea.AxisY.Title = "Memory (MB)"
   $chartarea.AxisX.Title = "Process Name"
   $chartarea.AxisY.Interval = 100
   $chartarea.AxisX.Interval = 1
   $chart1.ChartAreas.Add($chartarea)
 
# legend 
   $legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
   $legend.name = "Legend1"
   $chart1.Legends.Add($legend)
 
# data source
   $datasource = Get-Process | sort PrivateMemorySize -Descending  | Select-Object -First 5
 
# data series
   [void]$chart1.Series.Add("VirtualMem")
   $chart1.Series["VirtualMem"].ChartType = "Column"
   $chart1.Series["VirtualMem"].BorderWidth  = 3
   $chart1.Series["VirtualMem"].IsVisibleInLegend = $true
   $chart1.Series["VirtualMem"].chartarea = "ChartArea1"
   $chart1.Series["VirtualMem"].Legend = "Legend1"
   $chart1.Series["VirtualMem"].color = "#62B5CC"
   $datasource | ForEach-Object {$chart1.Series["VirtualMem"].Points.addxy( $_.Name , ($_.VirtualMemorySize / 1000000)) }
 
# data series
   [void]$chart1.Series.Add("PrivateMem")
   $chart1.Series["PrivateMem"].ChartType = "Column"
   $chart1.Series["PrivateMem"].IsVisibleInLegend = $true
   $chart1.Series["PrivateMem"].BorderWidth  = 3
   $chart1.Series["PrivateMem"].chartarea = "ChartArea1"
   $chart1.Series["PrivateMem"].Legend = "Legend1"
   $chart1.Series["PrivateMem"].color = "#E3B64C"
   $datasource | ForEach-Object {$chart1.Series["PrivateMem"].Points.addxy( $_.Name , ($_.PrivateMemorySize / 1000000)) }
 
# save chart
   $chart1.SaveImage("$scriptpath\SplineArea.png","png")
 

That’s it!

I hope this tutorial has helped you understand the basic concept of MCCs. What you’ve learned today is a good basis, but it’s just a fraction of what you can do with MCCs.

One last tip: an easy, ‘WYSIWYG’-way to experiment with all the different possibilities, chart styles and visual options, is to get a copy of Visual Studio Express. Just create a plain windows form, add a chart object and try out what all the properties are there for.

Have a nice day,
Denniver