Displaying a Squarified Treemap Using PowerShell and WPF

I was looking at VROPs recently and enjoyed looking at the data visualization that they used to show various states for things such as CPU, Memory and Disk Space for VMs and wanted to see if I could do the same thing in PowerShell, because why not?

vropsExample

After some searching on what this is, I found that it is called a Squarified Treemap that uses a heatmap, meaning that there are potentially 2 data points here being used to make up the visualization of data. The first data determines the size of the area of the square (or rectangle depending on how it turns out) and the second data set determines its color on the heat map. One is example would be your file system. You could have a large square to show the count of files in a folder but shows as green because the total file size isn’t that large compared to another folder which might have a smaller square to show its file count compared to others.

Starting out, I need to accomplish a few things if I want to make this:

  • Figure out the Squarified Treemap Algorithm
  • Determine the X and Y coordinates to be used on Canvas
  • Find formula to calculate when a color should be Red or Yellow or Green with Red signifying that it is bad.

Working the Squarified Treemap

Of course, none of this really turned out easy to accomplish. Even with the algorithm known for the squarified treemap, it seems it would work for a very simple case, but would quickly fall apart when I start straying from examples shown. Some of the places that I looked at for inspiration are here:

Reading all of these should give you a good idea on what I am trying to accomplish in PowerShell. Eventually I was able to figure out a good solution that didn’t really deal with trying to get as close as I could to the 1:1 aspect ratio but instead set a threshold on how much of the area of the rectangle that a single square could possible take up. I decided that I didn’t want more than 40% of the total area being used.

The end result is a helper function called New-SquareTreeMapData which takes the incoming data from a custom object or a single data point and starts using an algorithm to determine a number of things such as how big each rectangle will be as well as its X and Y coordinates on a window with a given Width and Height.

The helper function is available here:

Function New-SquareTreeMapData {
[cmdletbinding()]
Param (
[float]$Width,
[float]$Height,
[parameter(ValueFromPipeline=$True)]
[object[]]$InputObject,
[string]$LabelProperty,
[string]$DataProperty,
[string]$HeatmapProperty,
[int64]$MaxHeatMapSize
)
Begin {
Try {
[void][treemap.coordinate]
} Catch {
Add-Type -TypeDefinition @"
using System;
namespace TreeMap
{
public class Coordinate
{
public float X;
public float Y;

public Coordinate(float x, float y)
{
X = x;
Y = y;
}
public Coordinate(float x)
{
X = x;
Y = 0;
}
public Coordinate()
{
X = 0;
Y = 0;
}
public override string ToString()
{
return X+","+Y;
}
}
}
"@
}
Function Get-StartingOrientation {
Param ([float]$Width, [float]$Height)
Switch (($Width -ge $Height)) {
$True {'Vertical'}
$False {'Horizontal'}
}
}

#region Starting Data
$Row = 1
$Rectangle = New-Object System.Collections.ArrayList
$DecimalThreshold = .4
$TempData = New-Object System.Collections.ArrayList
If ($PSBoundParameters.ContainsKey('InputObject')) {
$Pipeline = $False
Write-Verbose "Adding `$InputObject to list"
[void]$TempData.AddRange($InputObject)
} Else {
$Pipeline = $True
}
#Sort the data
$List = New-Object System.Collections.ArrayList
$Stack = New-Object System.Collections.Stack
$FirstCoordRun = $True
$CurrentX = 0
$CurrentY = 0
#endregion Starting Data
}
Process {
If ($Pipeline) {
#Write-Verbose "Adding $($_) to list"
[void]$TempData.Add($_)
}
}
End {
If ($PSBoundParameters.ContainsKey('DataProperty')) {
$TempData | Sort-Object $DataProperty | ForEach {
#If it is 0, then it should not occupy space
If ($_.$DataProperty -gt 0) {
$Stack.Push($_)
}
}
} Else {
$TempData | Sort-Object | ForEach {
#If it is 0, then it should not occupy space
If ($_ -gt 0) {
$Stack.Push($_)
}
}
}
$ElementCount = $Stack.Count
#Begin building out the grid
$Temp = New-Object System.Collections.ArrayList

While ($Stack.Count -gt 0) {
$PreviousWidth = 0
$PreviousHeight = 0
$TotalBlockHeight = 0
$TotalBlockWidth = 0
Write-Verbose "Width: $Width - Height: $Height"
#Write-Verbose "StackCount: $($Stack.Count)"
$FirstRun = $True
#Write-Verbose "Row: $Row"
If ($PSBoundParameters.ContainsKey('DataProperty')) {
$TotalArea = ($Stack | Measure-Object -Property $DataProperty -Sum).Sum
} Else {
$TotalArea = ($Stack | Measure-Object -Sum).Sum
}
#Write-Verbose 'Getting starting orientation'
$Orientation = Get-StartingOrientation -Width $Width -Height $Height
Write-Verbose "Orientation: $Orientation"
$Iteration = 0
Do {
$Iteration++
#Write-Verbose "Iteration: $Iteration"
#Write-Verbose "TotalArea: $($TotalArea)"
[void]$List.Add($Stack.Pop())
If ($PSBoundParameters.ContainsKey('DataProperty')) {
$PercentArea = (($List | Measure-Object -Property $DataProperty -Sum).Sum/$TotalArea)
} Else {
$PercentArea = (($List | Measure-Object -Sum).Sum/$TotalArea)
}
#Write-Verbose "PercentArea: $($PercentArea)"
} Until (($PercentArea -ge $DecimalThreshold) -OR ($Stack.Count -eq 0))
#Write-Verbose "Threshold met!"
If ($List.Count -gt 1) {
If ($PSBoundParameters.ContainsKey('DataProperty')) {
$_area = ($List | Measure-Object -Property $DataProperty -Sum).Sum
} Else {
$_area = ($List | Measure-Object -Sum).Sum
}
}
$List | ForEach {
If ($PSBoundParameters.ContainsKey('DataProperty')) {
$Item = $_.$DataProperty
} Else {
$Item = $_
}
If ($PSBoundParameters.ContainsKey('LabelProperty')) {
$Label = $_.$LabelProperty
}
If ($PSBoundParameters.ContainsKey('HeatmapProperty')) {
$HeatmapData = $_.$HeatmapProperty
} ElseIf ($PSBoundParameters.ContainsKey('MaxHeatMapSize')){
$HeatmapData = $_
}
Switch ($Orientation) {
'Vertical' {
#Get block width
$BlockWidth = ($PercentArea * $Width)
Write-Verbose "BlockWidth: $($BlockWidth)"
If ($Iteration -eq 1) {
$BlockHeight = $Height
} Else {
#Get block height
$_percentarea = ($Item / $_area)
$BlockHeight = ($_percentarea*$Height)
Write-Verbose "BlockHeight: $($BlockHeight)"
}
}
'Horizontal' {
#Get block height
$BlockHeight = ($PercentArea * $Height)
Write-Verbose "BlockHeight: $($BlockHeight)"
If ($Iteration -eq 1) {
$BlockWidth = $Width
} Else {
#Get block width
$_percentarea = ($Item / $_area)
$BlockWidth = ($_percentarea*$Width)
Write-Verbose "BlockWidth: $($BlockWidth)"
}
}
}
If ($FirstCoordRun) {
Write-Verbose 'First run coordinates'
$Coordinate = New-Object -Typename treemap.coordinate -ArgumentList $CurrentX,$CurrentY
$FirstCoordRun=$False
} Else {
Write-Verbose 'Rest of coordinates'
Switch ($Orientation) {
'Vertical' {
Write-Verbose 'Setting Vertical coordinates'
Write-Verbose "TotalHeight: $($TotalBlockHeight)"
$Y = $TotalBlockHeight + $CurrentY
$Coordinate = New-Object -Typename treemap.coordinate -ArgumentList $CurrentX,$Y
}
'Horizontal' {
Write-Verbose "TotalWidth: $($TotalBlockWidth)"
Write-Verbose 'Setting Horizontal coordinates'
$X = $TotalBlockWidth + $CurrentX
$Coordinate = New-Object -Typename treemap.coordinate -ArgumentList $X,$CurrentY
}
}
}
[pscustomobject]@{
LabelProperty = $Label
DataProperty = $Item
HeatmapProperty = $HeatmapData
Row = $Row
Orientation = $Orientation
Width = $BlockWidth
Height = $BlockHeight
Coordinate = $Coordinate
ObjectData = $_
}
$PreviousWidth = $BlockWidth
$PreviousHeight = $BlockHeight
$TotalBlockHeight = $TotalBlockHeight + $BlockHeight
$TotalBlockWidth = $TotalBlockWidth + $BlockWidth
}
If ($Orientation -eq 'Vertical') {
$CurrentX = $BlockWidth + $CurrentX
$Width = $Width - $BlockWidth
} Else {
$CurrentY = $CurrentY + $BlockHeight
$Height = $Height - $BlockHeight
}
Write-Verbose "CurrentX: $($CurrentX)"
Write-Verbose "CurrentY: $($CurrentY)"
$list.Clear()
$Row++
$FirstCoordRun = $True
}
}
}

The gist of what it is doing is that we are determining the starting orientation to begin building out the squares based on the Width and Height. From there we start working through the areas of each data point and once we surpass the .4 that tells us to stop and begin processing the information that will contain our size, X/Y coordinates as well as some other information.

A quick demo of this:

#region Example using Filesystem against my current drive
$FileInfo = Get-ChildItem -Directory|ForEach {
$Files = Get-ChildItem $_.fullname -Recurse -File|measure-object -Sum -Property length
[pscustomobject]@{
Name = $_.name
Fullname = $_.fullname
Count = [int64]$Files.Count
Size = [int64]$Files.Sum
}
}
#endregion Example using Filesystem against my current drive
$Params = @{
Width = 600
Height = 200
LabelProperty = 'Fullname'
DataProperty = 'Count'
HeatmapProperty = 'Size'
}
$FileInfo | New-SquareTreeMapData @Params

image

Probably a little hard to see as a Table, but each object would look like this:

image

Here you can easily see that I have spots for my label  and data property, the row which is actually each iteration as it goes from the vertical to horizontal orientation. Also is the Width and Height of each data square and its coordinates. I also include the original object so we can create custom tooltips in the main function.

Determine the Heatmap Colors

Trying to determine how to handle the heatmap coloring was definitely a challenge. My goal was to utilize a Green –> Yellow –> Red transition to show the Good (Green) up to the Bad (Red). After some searching around, I came across this StackOverflow answer which provided a single line solution that came the closest to what I was looking for. I spent some time adjusting the code to meet my own expectations:

Function Color {
Param($Decimal)
If ($Decimal -gt 1) {
$Decimal = 1
}
$Red = ([float](2.0) * $Decimal)
$Red = If ($Red -gt 1) {
255
} Else {
$Red * 255
}
$Green = (([float](2.0) * (1 - $Decimal)))
$Green = If ($Green -gt 1) {
255
} Else {
$Green * 255
}
([windows.media.color]::FromRgb($Red, $Green, 0))
}

I opt to keep the Media.Color object and then convert it to its hexadecimal counterpart using ToString().

image

What I end up getting is a nice transition from my colors. Although I feel that the Green might be a little too light, I am not going worry about this at the moment because as of right now, it still works properly but is something that I would like to come back to and adjust.

The Final Product

In the end, I put together a function which utilizes all of these helper functions and techniques to accept data from either the pipeline or as a parameter along with supplying some other parameters (if needed) to create a UI that displays a squarified treemap as well as a heatmap if desired. I also allow optional tooltips to display on each square that can provide more information about the square when you hover the mouse over it.

Some examples of it in action:

#region Example using Filesystem against my current drive
$FileInfo = Get-ChildItem -Directory|ForEach {
$Files = Get-ChildItem $_.fullname -Recurse -File|measure-object -Sum -Property length
[pscustomobject]@{
Name = $_.name
Fullname = $_.fullname
Count = [int64]$Files.Count
Size = [int64]$Files.Sum
}
}
#endregion Example using Filesystem against my current drive

#region Create a custom tooltip
$Tooltip = {
@"
Fullname = $($This.LabelProperty)
FileCount = $($This.Dataproperty)
Size = $([math]::round(($This.HeatmapProperty/1MB),2)) MB
"@
}

#Create the UI
$Params = @{
Width = 600
Height = 200
LabelProperty = 'Fullname'
DataProperty = 'Count'
HeatmapProperty = 'Size'
Tooltip = $Tooltip
}
$FileInfo | Out-SquarifiedTreeMap @Params
#endregion Create a custom tooltip

 

image

Finding the Process using the most memory

#region Example using Process WorkingSet Memory
Get-Process |
Out-SquarifiedTreeMap -LabelProperty ProcessName -DataProperty WS -HeatmapProperty WS -Width 600 -Height 400
#endregion Example using Process WorkingSet Memory

image

Another example showing the MaxHeatmapSize parameter.

#region Example using just data and no object and specifying a heatmap threshold
1..8 | Out-SquarifiedTreeMap -Width 600 -Height 200 -MaxHeatMapSize 15
#endregion Example using just data and no object and specifying a heatmap threshold

image

Note that you don’t actually have to specify a heatmaproperty and it will default to a darker green color (this might be something worth changing in the future or adding another parameter to change this).

#region Not using a HeatMap
1..8 | Out-SquarifiedTreeMap -Width 600 -Height 200
#endregion Not using a HeatMap

image

So with that, feel free to give this a download and let me know what you think. The download links are below for both my GitHub source and the Technet Script Repository. I’m also interested in hearing how you might (or end up) using this in your day to day activities.

Source Code (Contributions always welcome!)

https://github.com/proxb/SquarifiedTreemap

Download Out-SquarifiedTreemap

https://gallery.technet.microsoft.com/scriptcenter/Out-SquarifiedTreemap-939fb379

 

Posted in powershell, WPF | Tagged , , , , | 4 Comments

Quick Hits: Determine User Connected to a Delegated PowerShell Endpoint

Just a quick blurb on how you can determine who is connected to a PowerShell remoting endpoint that you have configured to use a RunAsAccount. An example of doing this can be found here. When someone connects to an endpoint that is using a RunAsAccount, it will delegate the credentials of the service account and that means if you do something like ‘whoami’ while running in the session, you will get back the service account’s username.

SNAGHTML294d3d0

Here you can see where I connected to my remote server under the ‘prox-hyperv\proxb’ credentials, but because the endpoint is running as ‘prox-hyperv\endpointsvc’, when I call ‘whoami’, it comes back as the service account. Not that useful if we need to audit connections coming in.

Fortunately, the PowerShell team added an automatic variable called $PSSenderInfo which provides this information about who is making the connection to the endpoint. This is documented in the about_automatic_variables help file (it pays to read these Smile) and as shown below, we can now locate who is making the connection.

image

This is definitely something that you should keep in mind when creating remote endpoints for your admins to use when managing systems. This should be something that you have in your startup script that writes to an event log so you have a way of tracking connections.

Posted in powershell, Tips | Tagged , , , | Leave a comment

Create a Mouse Cursor Tracker using PowerShell and WPF

Sometimes when you have a little bit of free time, you decide to build things just to see if you can do it regardless of how useful it might actually be. I think this might be one of those occasions.

While working on a different side project, I somehow took a path that led me down looking at the X and Y coordinates of my mouse cursor in relation to its position on my window. Like some of the things that I start working on, I may not have a good reason to do it other than just for the simple fact of challenging myself to make it work.

Before I dive into what I did to make a “mouse tracker”, I wanted to explain my goal of actually making it. My goal is to have a small UI that can track my mouse movements in real time that would show my X and Y coordinates of the cursor based on its location on the window. It is important to know that the X coordinate is based on the position from the left of the window while the Y coordinate is based on the top of the window.

image

The process to get your mouse coordinates is actually pretty simple. We just have to look at the Position property in the System.Windows.Forms.Cursor class. Note that you will have to use Add-Type System.Windows.Forms if running from the console!

[System.Windows.Forms.Cursor]::Position

image

Note the X and Y coordinates above. This means that at the time that I ran the command, my mouse cursor was 54 pixels from the top of my screen and 362 pixels from the left of my screen. Now that we know how to get the mouse coordinates, the next step is to create our UI to display this on the screen.

I do want to add that my example code includes stuff related to runspaces because I make it a habit to put all of my UI stuff in runspaces for better responsiveness.

$Runspacehash = [hashtable]::Synchronized(@{})
$Runspacehash.host = $Host
$Runspacehash.runspace = [RunspaceFactory]::CreateRunspace()
$Runspacehash.runspace.ApartmentState = 'STA'
$Runspacehash.runspace.ThreadOptions = 'ReuseThread'
$Runspacehash.runspace.Open()
$Runspacehash.psCmd = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,System.Windows.Forms}.GetPowerShell()
$Runspacehash.runspace.SessionStateProxy.SetVariable('Runspacehash',$Runspacehash)
$Runspacehash.psCmd.Runspace = $Runspacehash.runspace
$Runspacehash.Handle = $Runspacehash.psCmd.AddScript({

Since I only care about showing the X and Y coordinates on the screen, I am going with a Grid that has 2 rows and 2 columns to show the coordinates. I also want to make this pretty small as I have no need to display a large UI just to display something like this. Being that I am using WPF as my UI, I will create the front end UI using XAML.

#Build the GUI
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" WindowStartupLocation = "CenterScreen"
Width = "80" Height = "50" ShowInTaskbar = "False" ResizeMode = "NoResize"
Topmost = "True" WindowStyle = "None" AllowsTransparency="True" Background="Transparent">
<Border BorderBrush="{x:Null}" BorderThickness="1" Height="50" Width="80" HorizontalAlignment="Left"
CornerRadius="15" VerticalAlignment="Top" Name="Border">
<Border.Background>
<LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
<LinearGradientBrush.GradientStops> <GradientStop Color='#C4CBD8' Offset='0' /> <GradientStop Color='#E6EAF5' Offset='0.2' />
<GradientStop Color='#CFD7E2' Offset='0.9' /> <GradientStop Color='#C4CBD8' Offset='1' /> </LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>
<Grid ShowGridLines='False'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = '*'/>
<RowDefinition Height = '*'/>
</Grid.RowDefinitions>
<Label x:Name='X_lbl' Tag = 'X' Grid.Column = '0' Grid.Row = '0' FontWeight = 'Bold'
HorizontalContentAlignment="Center">
<TextBlock TextDecorations="Underline"
Text="{Binding Path=Tag,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Label}}}"/>
</Label>
<Label x:Name='Y_lbl' Tag = 'Y' Grid.Column = '1' Grid.Row = '0' FontWeight = 'Bold'
HorizontalContentAlignment="Center">
<TextBlock TextDecorations="Underline"
Text="{Binding Path=Tag,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Label}}}"/>
</Label>
<Label x:Name='X_data_lbl' Grid.Column = '0' Grid.Row = '1' FontWeight = 'Bold'
HorizontalContentAlignment="Center" />
<Label x:Name='Y_data_lbl' Grid.Column = '1' Grid.Row = '1' FontWeight = 'Bold'
HorizontalContentAlignment="Center" />
</Grid>
</Border>
</Window>
"@

Now I can load up the XAML and connect to my controls that I will need for updating the data.

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#region Connect to Controls
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force -ErrorAction SilentlyContinue -Scope Script
}
#endregion Connect to Controls

At this point I have a UI that will work if we happened to run it.

[void]$Window.ShowDialog()
}).BeginInvoke()

That last line is just closing the loop with creating my runspace. But what we get is the following:

image

Getting there! You may have noticed that there is not a close button and if you run this, it may be difficult to move it around based on how I configured the window. I get around this by adding event handlers for the Right and Left mouse buttons to drag the window around and to close  the window.

$Window.Add_MouseRightButtonUp({
$This.close()
})
$Window.Add_MouseLeftButtonDown({
$This.DragMove()
})

We have the UI and the X and Y label already taken care of. The next piece of the puzzle is to get our coordinates to show up so we can start tracking our cursor position. This is where I had to think a little on how to ensure that the coordinates would update as I moved my mouse around. I looked at a number of events for the window but none of them really fit what I wanted to do. We had mouse enter and mouse leave events, but they only updated 1 time each time the event fired meaning that I wasn’t able to get the coordinates any other time.

In the end I decided to use a timer object attached to my window that will update every 10 milliseconds. I figure this was enough of a time that it would appear that the mouse tracker was updating the entire time while I move my mouse.

$Window.Add_SourceInitialized({
#Create Timer object
Write-Verbose 'Creating timer object'
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 1 minutes
Write-Verbose 'Adding 1 minute interval to timer object'
$timer.Interval = [TimeSpan]'0:0:0.01'
#Add event per tick
Write-Verbose 'Adding Tick Event to timer object'
$timer.Add_Tick({
$Mouse = [System.Windows.Forms.Cursor]::Position
$X_data_lbl.Content = $Mouse.x
$Y_data_lbl.Content = $Mouse.y
})
#Start timer
Write-Verbose 'Starting Timer'
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Window.Close()
}
})

Now I have a working mouse tracker to use for whatever you would like to do with it!

MouseTracker

 

Download MouseTracker

https://gallery.technet.microsoft.com/scriptcenter/PowerShell-Mouse-Cursor-2cde303d

Posted in powershell | Tagged , , , | 4 Comments

Guest Spot on Hey, Scripting Guy! talking PowerShell and Network Authentication Level Reporting

This was somewhat of a last minute article but it is a nice article on reporting on the Network Level Authentication configuration on systems using PowerShell. Be sure to check it out and let me know what you think!

Weekend Scripter: Report on Network Level Authentication
PowerTip: Find Expiring Certificates by Using PowerShell
Posted in powershell | Tagged , , , , | Leave a comment

Resources Available from my Talk on PowerShell and Pester

I had the great opportunity to speak at the user group that I happen to be the co-leader and co-founder (Omaha PowerShell User Group) of last week talking about PowerShell and Pester. This was the result of my own call for topics as I was going to speak about something, but wanted to leave it up to my fellow attendees to help determine the topic. Pester was the overwhelming majority, which was great for me because I wanted to get more into this as well!

I also had a lot of input online about recording this (we usually only record for remote speakers as my laptop is somewhat old) and that is also available to view as well.

Pester is a great framework to learn and understand and it is something that you will definitely not regret learning as you can start applying it to existing scripts and modules in your environment!

Video

Slide Deck and Example Code

http://1drv.ms/1Mj3mWI

Posted in powershell | Tagged , , , , | Leave a comment