Merge pull request #1219 from jhoneill/PivotTableUpdates

Pivot table updates
This commit is contained in:
Doug Finke
2022-07-16 13:25:17 -04:00
committed by GitHub
11 changed files with 251 additions and 9 deletions

133
Add-Subtotals.ps1 Normal file
View File

@@ -0,0 +1,133 @@
Function Add-Subtotals {
param(
[Parameter(Mandatory=$true, Position=0)]
$ChangeColumnName , # = "Location"
[Parameter(Mandatory=$true, Position=1)]
[hashtable]$AggregateColumn , #= @{"Sales" = "SUM" }
[Parameter(Position=2)]
$ExcelPath = ([System.IO.Path]::GetTempFileName() -replace "\.tmp", ".xlsx") ,
[Parameter(Position=3)]
$WorksheetName = "Sheet1",
[Parameter(ValueFromPipeline=$true)]
$InputObject, #$DataToPivot | Sort location, product
[switch]$HideSingleRows,
[switch]$NoSort,
[switch]$NoOutLine,
[switch]$Show
)
begin {
if (-not $PSBoundParameters.ContainsKey('ExcelPath')) {$Show = $true}
$data = @()
$aggFunctions = [ordered]@{
"AVERAGE" = 1; "COUNT" = 2; "COUNTA" = 3 #(non empty cells) f
"MAX" = 4; "MIN" = 5; "PRODUCT" = 6; "STDEV" = 7 # (sample)
"STDEVP" = 8 # (whole population);
"SUM" = 9; "VAR" = 10 # (Variance sample)
"VARP" = 11 # (whole population) #add 100 to ignore hidden cells
}
}
process {
$data += $InputObject
}
end {
if (-not $NoSort) {$data = $data | Sort-Object $changeColumnName}
$Header = $data[0].PSObject.Properties.Name
#region turn each entry in $AggregateColumn "=SUBTOTAL(a,x{0}}:x{1})" where a is the aggregate function number and x is the column letter
$aggFormulas = @{}
foreach ($k in $AggregateColumn.Keys) {
$columnNo = 0 ;
while ($columnNo -lt $header.count -and $header[$columnNo] -ne $k) {$columnNo ++}
if ($columnNo -eq $header.count) {
throw "'$k' isn't a property of the first row of data."; return
}
if ($AggregateColumn[$k] -is [string]) {
$aggfn = $aggFunctions[$AggregateColumn[$k]]
if (-not $aggfn) {
throw "$($AggregateColumn[$k]) is not a valid aggregation function - these are $($aggFunctions.keys -join ', ')" ; return
}
}
else {$aggfn = $AggregateColumn[$k]}
$aggFormulas[$k] = "=SUBTOTAL({0},{1}{{0}}:{1}{{1}})" -f $aggfn , (Get-ExcelColumnName ($columnNo+1) ).ColumnName
}
if ($aggformulas.count -lt 1) {throw "We didn't get any aggregation formulas"}
$aggFormulas | out-string -Stream | Write-Verbose -Verbose
#endregion
$insertedRows = @()
$singleRows = @()
$previousValue = $data[0].$changeColumnName
$currentRow = $lastChangeRow = 2
#region insert subtotals and send to excel:
#each time there is a change in the column we're intetersted in.
#either Add a row with the value and subtotal(s) function(s) if there is more than one row to total
#or note the row if there was only one row with that value (we may hide it later.)
$excel = $data |
ForEach-Object -process {
if ($_.$changeColumnName -ne $previousValue) {
if ($lastChangeRow -lt ($currentrow - 1)) {
$NewObj = @{$changeColumnName = $previousValue}
foreach ($k in $aggFormulas.Keys) {
$newobj[$k] = $aggformulas[$k] -f $lastChangeRow, ($currentRow - 1)
}
$insertedRows += $currentRow
[pscustomobject]$newobj
$currentRow += 1
}
else {$singleRows += $currentRow }
$lastChangeRow = $currentRow
$previousValue = $_.$changeColumnName
}
$_
$currentRow += 1
} -end { # the process block won't output the last row
if ($lastChangeRow -lt ($currentrow - 1)) {
$NewObj = @{$changeColumnName = $previousValue}
foreach ($k in $aggFormulas.Keys) {
$newobj[$k] = $aggformulas[$k] -f $lastChangeRow, ($currentRow - 1)
}
$insertedRows += $currentRow
[pscustomobject]$newobj
}
else {$singleRows += $currentRow }
} | Export-Excel -Path $ExcelPath -PassThru -AutoSize -AutoFilter -AutoNameRange -BoldTopRow -WorksheetName $WorksheetName -Activate -ClearSheet #-MaxAutoSizeRows 10000
#endregion
#Put the subtotal rows in bold optionally hide rows where only one has the value of interest.
$ws = $excel.$WorksheetName
#We kept lists of the total rows Since 1 rows won't get expand/collapse we can hide them.
foreach ($r in $insertedrows) {$ws.Row($r).style.font.bold = $true }
if ($HideSingleRows) {
foreach ($r in $hideRows) { $ws.Row($r).hidden = $true}
}
$range = $ws.Dimension.Address
$ExcelPath = $excel.File.FullName
$SheetIndex = $ws.index
if ($NoOutline) {
Close-ExcelPackage $excel -show:$Show
return
}
else {
Close-ExcelPackage $excel
try { $excelApp = New-Object -ComObject "Excel.Application" }
catch { Write-Warning "Could not start Excel application - which usually means it is not installed." ; return }
try { $excelWorkBook = $excelApp.Workbooks.Open($ExcelPath) }
catch { Write-Warning -Message "Could not Open $ExcelPath." ; return }
$ws = $excelWorkBook.Worksheets.item($SheetIndex)
$null = $ws.Range($range).Select()
$null = $excelapp.ActiveCell.AutoOutline()
$null = $ws.Outline.ShowLevels(1,$null)
$excelWorkBook.Save()
if ($show) {$excelApp.Visible = $true}
else {
[void]$excelWorkBook.close()
$excelapp.Quit()
}
}
}
}

View File

@@ -6,15 +6,16 @@
#>
param(
[Parameter(Mandatory=$true)]
$moduleName,
$moduleName = "ImportExcel",
[ValidateSet('Column','Bar','Line','Pie')]
$chartType="Line"
)
$galleryUrl = "https://www.powershellgallery.com/packages/$moduleName"
$nolegend = '-nolegend'
if($chartType -eq 'pie') {$nolegend = $null}
$code = "$($chartType)Chart (Get-HtmlTable $galleryUrl -FirstDataRow 1 | sort lastupdated -desc) -title 'Download stats for $moduleName' $nolegend"
$download = Get-HtmlTable "https://www.powershellgallery.com/packages/$moduleName" -FirstDataRow 1 |
Select-Object @{n="Version";e={$v = $Null ; if ($_.version -is [valuetype]) {[string][version]($_.version.tostring("0.0")) }
elseif ($_.version -is [string] -and [version]::TryParse($_.version.trim(),[ref]$v)) {$v}
else {$_.Version.trim() -replace "\s+"," " } }},
Downloads, @{n="LastUpdated";e={[datetime]$_.last_updated}} |
Sort-Object lastupdated -Descending
$code | Invoke-Expression
& "$($chartType)Chart" $download "Download stats for $moduleName" -nolegend:($chartype -ne 'pie')

View File

@@ -0,0 +1,13 @@
try {Import-Module $PSScriptRoot\..\..\ImportExcel.psd1} catch {throw ; return}
#Get rid of pre-exisiting sheet
$xlSourcefile = "$env:TEMP\ImportExcelExample.xlsx"
Write-Verbose -Verbose -Message "Save location: $xlSourcefile"
Remove-Item $xlSourcefile -ErrorAction Ignore
$PivotTableDefinition = New-PivotTableDefinition -Activate -PivotTableName Points `
-PivotRows Driver -PivotColumns Date -PivotData @{Points = "SUM"} -GroupDateColumn Date -GroupDatePart Years, Months
Import-Csv "$PSScriptRoot\First10Races.csv" |
Select-Object Race, @{n = "Date"; e = {[datetime]::ParseExact($_.date, "dd/MM/yyyy", (Get-Culture))}}, FinishPosition, Driver, GridPosition, Team, Points |
Export-Excel $xlSourcefile -Show -AutoSize -PivotTableDefinition $PivotTableDefinition

View File

@@ -0,0 +1,13 @@
try {Import-Module $PSScriptRoot\..\..\ImportExcel.psd1} catch {throw ; return}
#Get rid of pre-exisiting sheet
$xlSourcefile = "$env:TEMP\ImportExcelExample.xlsx"
Write-Verbose -Verbose -Message "Save location: $xlSourcefile"
Remove-Item $xlSourcefile -ErrorAction Ignore
$PivotTableDefinition = New-PivotTableDefinition -Activate -PivotTableName Places `
-PivotRows Driver -PivotColumns FinishPosition -PivotData @{Date = "Count"} -GroupNumericColumn FinishPosition -GroupNumericMin 1 -GroupNumericMax 25 -GroupNumericInterval 3
Import-Csv "$PSScriptRoot\First10Races.csv" |
Select-Object Race, @{n = "Date"; e = {[datetime]::ParseExact($_.date, "dd/MM/yyyy", (Get-Culture))}}, FinishPosition, Driver, GridPosition, Team, Points |
Export-Excel $xlSourcefile -Show -AutoSize -PivotTableDefinition $PivotTableDefinition

View File

@@ -18,8 +18,10 @@
[String]$PivotTotals = "Both",
[Switch]$NoTotalsInPivot,
[String]$GroupDateRow,
[String]$GroupDateColumn,
[OfficeOpenXml.Table.PivotTable.eDateGroupBy[]]$GroupDatePart,
[String]$GroupNumericRow,
[String]$GroupNumericColumn,
[double]$GroupNumericMin = 0 ,
[double]$GroupNumericMax = [Double]::MaxValue ,
[double]$GroupNumericInterval = 100 ,
@@ -139,11 +141,21 @@
if (-not $r ) {Write-Warning -Message "Could not find a Row field named '$GroupNumericRow'; no numeric grouping will be done."}
else {$r.AddNumericGrouping($GroupNumericMin, $GroupNumericMax, $GroupNumericInterval)}
}
elseif ($GroupNumericColumn) {
$c = $pivotTable.ColumnFields.Where( {$_.name -eq $GroupNumericColumn })
if (-not $c ) {Write-Warning -Message "Could not find a Column field named '$GroupNumericColumn'; no numeric grouping will be done."}
else {$c.AddNumericGrouping($GroupNumericMin, $GroupNumericMax, $GroupNumericInterval)}
}
if ($GroupDateRow -and $PSBoundParameters.ContainsKey("GroupDatePart")) {
$r = $pivotTable.RowFields.Where( {$_.name -eq $GroupDateRow })
if (-not $r ) {Write-Warning -Message "Could not find a Row field named '$GroupDateRow'; no date grouping will be done."}
else {$r.AddDateGrouping($GroupDatePart)}
}
elseif ($GroupDateColumn -and $PSBoundParameters.ContainsKey("GroupDatePart")) {
$c = $pivotTable.ColumnFields.Where( {$_.name -eq $GroupDateColumn })
if (-not $c ) {Write-Warning -Message "Could not find a Column field named '$GroupDateColumn'; no date grouping will be done."}
else {$c.AddDateGrouping($GroupDatePart)}
}
}
catch {Write-Warning -Message "Failed adding PivotTable '$pivotTableName': $_"}
}

View File

@@ -16,8 +16,10 @@ function New-PivotTableDefinition {
[String]$PivotTotals = "Both",
[Switch]$NoTotalsInPivot,
[String]$GroupDateRow,
[String]$GroupDateColumn,
[OfficeOpenXml.Table.PivotTable.eDateGroupBy[]]$GroupDatePart,
[String]$GroupNumericRow,
[String]$GroupNumericColumn,
[double]$GroupNumericMin = 0 ,
[double]$GroupNumericMax = [Double]::MaxValue ,
[double]$GroupNumericInterval = 100 ,

View File

@@ -65,6 +65,11 @@ Plus, wiring the [PowerShell ScriptAnalyzer Excel report](https://github.com/dfi
![](.gitbook/assets/ScriptAnalyzerReport.png)
## What's new for 7.1.4
- Added GroupNumericColumn and GroupDateColumn to New-PivotTableDefinition and Add-PivotTable.
## What's new 7.1.3
- Changed to `ProviderPath`. Thanks [Trevor Walker](https://github.com/sporkabob)

View File

@@ -331,6 +331,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupDateColumn
The name of a Column field which should be grouped by parts of the date/time \(ignored if GroupDateRow is not specified\)
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupDatePart
The Part\(s\) of the date to use in the grouping \(ignored if GroupDateRow is not specified\)
@@ -364,6 +380,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupNumericColumn
The name of a Column field which should be grouped by Number \(e.g. 0-99, 100-199, 200-299 \)
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupNumericMin
The starting point for grouping

View File

@@ -225,6 +225,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupDateColumn
The name of a column field which should be grouped by parts of the date/time \(ignored if GroupDatePart is not specified\)
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupDatePart
The Part\(s\) of the date to use in the grouping \(ignored if GroupDateRow is not specified\)
@@ -234,7 +250,6 @@ Type: eDateGroupBy[]
Parameter Sets: (All)
Aliases:
Accepted values: Years, Quarters, Months, Days, Hours, Minutes, Seconds
Required: False
Position: Named
Default value: None
@@ -250,7 +265,6 @@ The name of a row field which should be grouped by Number \(e.g 0-99, 100-199, 2
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
@@ -258,6 +272,23 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupNumericColumn
The name of a column field which should be grouped by Number \(e.g 0-99, 100-199, 200-299 \)
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GroupNumericMin
The starting point for grouping