Create Interactive Charts with WPF and PowerShell

So I’m not a big Twitter fan, but I do admit – as an IT professional you can find a lot of useful and pertinent information there. One example was this morning when I happened to notice a tweet from Microsoft about their opensource projects on Github. After a quick perusal, I happened across an interesting project called Interactive Data Display for WPF. According to its description:

Interactive Data Display for WPF is a set of controls for adding interactive visualization of dynamic data to your application. It allows to create line graphs, bubble charts, heat maps and other complex 2D plots which are very common in scientific software. Interactive Data Display for WPF integrates well with Bing Maps control to show data on a geographic map in latitude/longitude coordinates. The controls can also be operated programmatically.

There are some nice-looking chart examples there such as:

sinline

markers

barchart (1)

Since there are no native charting controls in WPF this was of interest, so I fired up my PowerShell ISE and tried to get this working.

I created the following simple example using a bar chart. You can change the X or Y values then click Plot to update the chart.

Chart

The nice thing with this control is that it’s interactive – you can scroll the mouse wheel to zoom in and out, as well as move the axis left and right, and double-click to re-center.

barchartinteractive

Here’s the POSH code for the example:

There are a number of dependency libraries that the script will download for you, or you can also install them via the NuGet gallery as indicated in the project’s readme.

This is just a quick demo, but it’s a pretty cool control!

Creating Simple Charts in WPF with PowerShell

Windows Presentation Foundation (WPF) is great for creating GUI applications, but it does not natively contain any charting controls.  There are a number of products that can be used to create charts in WPF, including the WPF toolkit and the Microsoft Chart Controls for .Net, but good-old Windows Forms does this natively.

WPF has does have a WindowsFormsHost control, but there are a number of potential issues with hosting Windows Forms in a WPF application, and it not recommended practice.

After some playing around however, I found it is possible to add a Windows Forms chart simply by displaying it as an image.  Furthermore, it is also possible to save the image to a memory stream in binary format, which means it does not need to be saved to disk as a file, but can simply be stored and read from memory in binary form.

Below is a simple example of a Windows Forms pie chart added as an image to a WPF window using PowerShell.  It calculates the used and available RAM in the local system, creates a pie chart from the data, saves it as in image in binary form, then adds it as the source to an image control in the WPF window.  Pretty cool 🙂

chart


# Add required assemblies
Add-Type -AssemblyName PresentationFramework,System.Windows.Forms,System.Windows.Forms.DataVisualization

# Create WPF window
[xml]$xaml = @"
<Window          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"         Title="Chart Example" Height="350" Width="420">
    <Grid>
        <Image x:Name="image" HorizontalAlignment="Left" Height="auto" VerticalAlignment="Top" Width="auto"/>
    </Grid>
</Window>

"@

# Add window and it's named elements to a hash table
$script:hash = @{}
$hash.Window = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $xaml))
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object -Process {
    $hash.$($_.Name) = $hash.Window.FindName($_.Name)
}

# Function to create a Windows Forms pie chart
# Modified from https://www.simple-talk.com/sysadmin/powershell/building-a-daily-systems-report-email-with-powershell/
Function Create-PieChart() {
    param([hashtable]$Params)

    #Create our chart object
    $Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart
    $Chart.Width = 430
    $Chart.Height = 330
    $Chart.Left = 10
    $Chart.Top = 10

    #Create a chartarea to draw on and add this to the chart
    $ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $Chart.ChartAreas.Add($ChartArea)
    [void]$Chart.Series.Add("Data") 

    #Add a datapoint for each value specified in the parameter hash table
    $Params.GetEnumerator() | foreach {
        $datapoint = new-object System.Windows.Forms.DataVisualization.Charting.DataPoint(0, $_.Value.Value)
        $datapoint.AxisLabel = "$($_.Value.Header)" + "(" + $($_.Value.Value) + " GB)"
        $Chart.Series["Data"].Points.Add($datapoint)
    }

    $Chart.Series["Data"].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Pie
    $Chart.Series["Data"]["PieLabelStyle"] = "Outside"
    $Chart.Series["Data"]["PieLineColor"] = "Black"
    $Chart.Series["Data"]["PieDrawingStyle"] = "Concave"
    ($Chart.Series["Data"].Points.FindMaxByValue())["Exploded"] = $true

    #Set the title of the Chart
    $Title = new-object System.Windows.Forms.DataVisualization.Charting.Title
    $Chart.Titles.Add($Title)
    $Chart.Titles[0].Text = "RAM Usage Chart ($($env:COMPUTERNAME))"

    #Save the chart to a memory stream, then to the hash table as a byte array
    $Stream = New-Object System.IO.MemoryStream
    $Chart.SaveImage($Stream,"png")
    $Hash.Stream = $Stream.GetBuffer()
    $Stream.Dispose()
}

# Add an event to display the chart when the window is opened
$hash.Window.Add_ContentRendered({
    # Create a hash table to store values
    $Params = @{}
    # Get local RAM usage from WMI
    $RAM = (Get-CimInstance -ClassName Win32_OperatingSystem -Property TotalVisibleMemorySize,FreePhysicalMemory)
    # Add Free RAM to a hash table
    $Params.FreeRam = @{}
    $Params.FreeRam.Header = "Free RAM"
    $Params.FreeRam.Value = [math]::Round(($RAM.FreePhysicalMemory / 1MB),2)
    # Add used RAM to a hash table
    $Params.UsedRam = @{}
    $Params.UsedRam.Header = "Used RAM"
    $Params.UsedRam.Value = [math]::Round((($RAM.TotalVisibleMemorySize / 1MB) - ($RAM.FreePhysicalMemory / 1MB)),2)
    # Create the Chart
    Create-PieChart $Params
    # Set the image source
    $Hash.image.Source = $hash.Stream
})

# Display window
$null = $hash.Window.ShowDialog()