KaranReportTools.psm1

function Show-GridChartUI {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,

        [string]$Title = "Grid + Charts"
    )

    begin {
        $items = [System.Collections.Generic.List[object]]::new()
    }
    process {
        if ($null -ne $InputObject) { [void]$items.Add($InputObject) }
    }
    end {
        # Assemblies for WinForms usage (NOT compiling C#)
        Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
        Add-Type -AssemblyName System.Drawing      -ErrorAction Stop
        Add-Type -AssemblyName System.Data         -ErrorAction Stop

        # Optional charting
        $chartsAvailable = $true
        try {
            Add-Type -AssemblyName System.Windows.Forms.DataVisualization -ErrorAction Stop
        } catch {
            $chartsAvailable = $false
        }

        # Convert pipeline objects to DataTable
        $dt = New-Object System.Data.DataTable "Input"
        if ($items.Count -gt 0) {
            $props = $items[0].PSObject.Properties | Where-Object { $_.MemberType -in 'NoteProperty','Property' }
            foreach ($p in $props) { [void]$dt.Columns.Add($p.Name, [object]) }

            foreach ($obj in $items) {
                $row = $dt.NewRow()
                foreach ($p in $props) { $row[$p.Name] = $obj.$($p.Name) }
                [void]$dt.Rows.Add($row)
            }
        }

        # DataView for filtering
        $view = New-Object System.Data.DataView($dt)
        $bs   = New-Object System.Windows.Forms.BindingSource
        $bs.DataSource = $view

        # ---------------- UI ----------------
        $form = New-Object System.Windows.Forms.Form
        $form.Text = $Title
        $form.StartPosition = 'CenterScreen'
        $form.Width  = 1350
        $form.Height = 820
        $form.KeyPreview = $true

        $main = New-Object System.Windows.Forms.SplitContainer
        $main.Dock = 'Fill'
        $main.Orientation = 'Vertical'
        $main.SplitterDistance = 980
        $form.Controls.Add($main)

        # LEFT layout
        $left = New-Object System.Windows.Forms.TableLayoutPanel
        $left.Dock = 'Fill'
        $left.RowCount = 4
        $left.ColumnCount = 1
        [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 42)))
        [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 55)))
        [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 44)))
        [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 45)))
        $main.Panel1.Controls.Add($left)

        # Search panel
        $searchPanel = New-Object System.Windows.Forms.FlowLayoutPanel
        $searchPanel.Dock = 'Fill'
        $searchPanel.Padding = New-Object System.Windows.Forms.Padding(8,8,8,0)
        $searchPanel.WrapContents = $false

        $lblSearch = New-Object System.Windows.Forms.Label
        $lblSearch.Text = "Search:"
        $lblSearch.AutoSize = $true
        $lblSearch.Padding = New-Object System.Windows.Forms.Padding(0,6,6,0)
        $searchPanel.Controls.Add($lblSearch)

        $txtSearch = New-Object System.Windows.Forms.TextBox
        $txtSearch.Width = 260
        $searchPanel.Controls.Add($txtSearch)

        $lblCol = New-Object System.Windows.Forms.Label
        $lblCol.Text = "Column:"
        $lblCol.AutoSize = $true
        $lblCol.Padding = New-Object System.Windows.Forms.Padding(12,6,6,0)
        $searchPanel.Controls.Add($lblCol)

        $cmbSearchCol = New-Object System.Windows.Forms.ComboBox
        $cmbSearchCol.Width = 220
        $cmbSearchCol.DropDownStyle = 'DropDownList'
        $searchPanel.Controls.Add($cmbSearchCol)

        $btnClear = New-Object System.Windows.Forms.Button
        $btnClear.Text = "Clear"
        $btnClear.Width = 80
        $btnClear.Height = 26
        $btnClear.Margin = New-Object System.Windows.Forms.Padding(12,4,0,0)
        $searchPanel.Controls.Add($btnClear)

        # --- Row count label (Showing / Total / Filtered out) ---
        $lblCount = New-Object System.Windows.Forms.Label
        $lblCount.AutoSize = $true
        $lblCount.Padding  = New-Object System.Windows.Forms.Padding(16,6,0,0)
        $lblCount.ForeColor = [System.Drawing.Color]::DimGray
        $lblCount.Text = ""
        $searchPanel.Controls.Add($lblCount)

        $left.Controls.Add($searchPanel, 0, 0)

        # Grid
        $grid = New-Object System.Windows.Forms.DataGridView
        $grid.Dock = 'Fill'
        $grid.ReadOnly = $true
        $grid.AllowUserToAddRows = $false
        $grid.AllowUserToDeleteRows = $false
        $grid.AutoGenerateColumns = $true
        $grid.AutoSizeColumnsMode = 'DisplayedCells'
        $grid.SelectionMode = 'FullRowSelect'
        $grid.MultiSelect = $false
        $grid.DataSource = $bs
        $left.Controls.Add($grid, 0, 1)

        # Export panel
        $exportPanel = New-Object System.Windows.Forms.FlowLayoutPanel
        $exportPanel.Dock = 'Fill'
        $exportPanel.Padding = New-Object System.Windows.Forms.Padding(8,6,8,0)
        $exportPanel.WrapContents = $false

        $btnExportCsv = New-Object System.Windows.Forms.Button
        $btnExportCsv.Text = "Export CSV (filtered)"
        $btnExportCsv.Width = 170
        $exportPanel.Controls.Add($btnExportCsv)

        $btnExportXlsx = New-Object System.Windows.Forms.Button
        $btnExportXlsx.Text = "Export XLSX (requires Excel)"
        $btnExportXlsx.Width = 220
        $btnExportXlsx.Margin = New-Object System.Windows.Forms.Padding(10,0,0,0)
        $exportPanel.Controls.Add($btnExportXlsx)

        $hint = New-Object System.Windows.Forms.Label
        $hint.Text = "CSV always works. XLSX needs Excel installed."
        $hint.AutoSize = $true
        $hint.Padding = New-Object System.Windows.Forms.Padding(14,6,0,0)
        $hint.ForeColor = [System.Drawing.Color]::DimGray
        $exportPanel.Controls.Add($hint)

        $left.Controls.Add($exportPanel, 0, 2)

        # Chart area
        $chartSplit = New-Object System.Windows.Forms.SplitContainer
        $chartSplit.Dock = 'Fill'
        $chartSplit.Orientation = 'Horizontal'
        $chartSplit.SplitterDistance = 270
        $left.Controls.Add($chartSplit, 0, 3)

        $chart = $null
        $cmbChartType = $null
        $cmbAgg = $null
        $cmbX = $null
        $cmbY = $null
        $btnPlot = $null
        $btnClearChart = $null

        if ($chartsAvailable) {
            $chart = New-Object System.Windows.Forms.DataVisualization.Charting.Chart
            $chart.Dock = 'Fill'
            $chart.ChartAreas.Clear()
            [void]$chart.ChartAreas.Add("Main")
            $chartSplit.Panel1.Controls.Add($chart)

            # Minor default styling
            $chart.BackColor = [System.Drawing.Color]::White
            $chart.ChartAreas["Main"].BackColor = [System.Drawing.Color]::White
            $chart.ChartAreas["Main"].AxisX.MajorGrid.Enabled = $false
            $chart.ChartAreas["Main"].AxisY.MajorGrid.LineColor = [System.Drawing.Color]::Gainsboro
            $chart.ChartAreas["Main"].AxisX.Interval = 1

            $chartControls = New-Object System.Windows.Forms.FlowLayoutPanel
            $chartControls.Dock = 'Fill'
            $chartControls.Padding = New-Object System.Windows.Forms.Padding(8)
            $chartControls.WrapContents = $true
            $chartSplit.Panel2.Controls.Add($chartControls)

            $cmbChartType = New-Object System.Windows.Forms.ComboBox
            $cmbChartType.DropDownStyle = 'DropDownList'
            $cmbChartType.Width = 130
            [void]$cmbChartType.Items.AddRange(@("Column","Bar","Line","Pie","Area"))
            $cmbChartType.SelectedIndex = 0

            $cmbAgg = New-Object System.Windows.Forms.ComboBox
            $cmbAgg.DropDownStyle = 'DropDownList'
            $cmbAgg.Width = 110
            [void]$cmbAgg.Items.AddRange(@("Sum","Avg","Count","Raw"))
            $cmbAgg.SelectedIndex = 0

            $cmbX = New-Object System.Windows.Forms.ComboBox
            $cmbX.DropDownStyle = 'DropDownList'
            $cmbX.Width = 200

            $cmbY = New-Object System.Windows.Forms.ComboBox
            $cmbY.DropDownStyle = 'DropDownList'
            $cmbY.Width = 200

            $btnPlot = New-Object System.Windows.Forms.Button
            $btnPlot.Text = "Plot"
            $btnPlot.Width = 90
            $btnPlot.Margin = New-Object System.Windows.Forms.Padding(16,2,0,0)

            $btnClearChart = New-Object System.Windows.Forms.Button
            $btnClearChart.Text = "Clear chart"
            $btnClearChart.Width = 110
            $btnClearChart.Margin = New-Object System.Windows.Forms.Padding(8,2,0,0)

            $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Chart:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(0,6,6,0))}))
            $chartControls.Controls.Add($cmbChartType)
            $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="X:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))}))
            $chartControls.Controls.Add($cmbX)
            $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Y:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))}))
            $chartControls.Controls.Add($cmbY)
            $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Agg:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))}))
            $chartControls.Controls.Add($cmbAgg)
            $chartControls.Controls.Add($btnPlot)
            $chartControls.Controls.Add($btnClearChart)
        }
        else {
            $chartMsg = New-Object System.Windows.Forms.Label
            $chartMsg.Dock = 'Fill'
            $chartMsg.TextAlign = 'MiddleCenter'
            $chartMsg.ForeColor = [System.Drawing.Color]::DimGray
            $chartMsg.Text = "Charts not available (System.Windows.Forms.DataVisualization not found).`nYou can still export XLSX and chart in Excel."
            $chartSplit.Panel1.Controls.Add($chartMsg)
        }

        # RIGHT layout (columns + rename)
        $right = New-Object System.Windows.Forms.TableLayoutPanel
        $right.Dock = 'Fill'
        $right.RowCount = 4
        $right.ColumnCount = 1
        [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 28)))
        [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 70)))
        [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 28)))
        [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 60)))
        $main.Panel2.Controls.Add($right)

        $right.Controls.Add((New-Object System.Windows.Forms.Label -Property @{
            Text="Columns (toggle visibility):"; Dock='Fill'; Padding=(New-Object System.Windows.Forms.Padding(8,6,0,0))
        }), 0, 0)

        $clbCols = New-Object System.Windows.Forms.CheckedListBox
        $clbCols.Dock = 'Fill'
        $clbCols.CheckOnClick = $true
        $right.Controls.Add($clbCols, 0, 1)

        $right.Controls.Add((New-Object System.Windows.Forms.Label -Property @{
            Text="Rename selected column header:"; Dock='Fill'; Padding=(New-Object System.Windows.Forms.Padding(8,6,0,0))
        }), 0, 2)

        $renamePanel = New-Object System.Windows.Forms.FlowLayoutPanel
        $renamePanel.Dock = 'Fill'
        $renamePanel.Padding = New-Object System.Windows.Forms.Padding(8)
        $renamePanel.WrapContents = $false

        $txtHeader = New-Object System.Windows.Forms.TextBox
        $txtHeader.Width = 210
        $renamePanel.Controls.Add($txtHeader)

        $btnRename = New-Object System.Windows.Forms.Button
        $btnRename.Text = "Rename"
        $btnRename.Width = 90
        $renamePanel.Controls.Add($btnRename)

        $right.Controls.Add($renamePanel, 0, 3)

        # ---------- Populate column UI ----------
        $cmbSearchCol.Items.Clear()
        [void]$cmbSearchCol.Items.Add("(All columns)")
        $clbCols.Items.Clear()

        if ($chartsAvailable) {
            $cmbX.Items.Clear()
            $cmbY.Items.Clear()
        }

        foreach ($col in $dt.Columns) {
            [void]$cmbSearchCol.Items.Add($col.ColumnName)
            [void]$clbCols.Items.Add($col.ColumnName, $true)
            if ($chartsAvailable) {
                [void]$cmbX.Items.Add($col.ColumnName)
                [void]$cmbY.Items.Add($col.ColumnName)
            }
        }

        $cmbSearchCol.SelectedIndex = 0
        if ($chartsAvailable -and $cmbX.Items.Count -gt 0) { $cmbX.SelectedIndex = 0 }
        if ($chartsAvailable -and $cmbY.Items.Count -gt 1) { $cmbY.SelectedIndex = 1 }
        elseif ($chartsAvailable -and $cmbY.Items.Count -gt 0) { $cmbY.SelectedIndex = 0 }

        # ---------- Event helpers ----------

        # --- Updates the "Showing / Total" indicator and the window title ---
        $updateCounts = {
            $total = $dt.Rows.Count
            $shown = $bs.Count
            $filteredOut = $total - $shown

            if ($filteredOut -gt 0) {
                $lblCount.Text = "Showing $shown / $total (filtered out $filteredOut)"
            }
            else {
                $lblCount.Text = "Showing $shown / $total"
            }

            $form.Text = "$Title - $shown/$total rows"
        }

        $applyFilter = {
            $q = ($txtSearch.Text ?? "").Trim()

            if ([string]::IsNullOrEmpty($q)) {
                $view.RowFilter = ""
                & $updateCounts
                return
            }

            $q = $q.Replace("'", "''")  # escape

            $selected = $null
            if ($cmbSearchCol.SelectedIndex -gt 0 -and $cmbSearchCol.SelectedItem) {
                $selected = $cmbSearchCol.SelectedItem.ToString()
            }

            if ($selected) {
                $view.RowFilter = "Convert([$selected], 'System.String') LIKE '%$q%'"
                & $updateCounts
                return
            }

            $parts = New-Object System.Collections.Generic.List[string]
            foreach ($c in $dt.Columns) {
                [void]$parts.Add("Convert([$($c.ColumnName)], 'System.String') LIKE '%$q%'")
            }
            $view.RowFilter = [string]::Join(" OR ", $parts.ToArray())
            & $updateCounts
        }

        # Wire search/filter events
        $txtSearch.Add_TextChanged($applyFilter)
        $cmbSearchCol.Add_SelectedIndexChanged($applyFilter)

        $btnClear.Add_Click({
            $txtSearch.Text = ""
            $cmbSearchCol.SelectedIndex = 0
            & $updateCounts
        })

        # Keep counts updated if the underlying binding list changes
        $bs.Add_ListChanged({ & $updateCounts })

        # Initial counts
        & $updateCounts

        # ---- Column visibility toggle ----
        $clbCols.Add_ItemCheck({
            param($s, $e)

            $idx = $e.Index
            if ($idx -isnot [int] -or $idx -lt 0 -or $idx -ge $clbCols.Items.Count) { return }

            $item = $clbCols.Items[$idx]
            if ($null -eq $item) { return }

            $name = $item.ToString()
            if ([string]::IsNullOrWhiteSpace($name)) { return }

            if ($grid.Columns.Contains($name)) {
                $grid.Columns[$name].Visible = ($e.NewValue -eq [System.Windows.Forms.CheckState]::Checked)
            }
        })

        $clbCols.Add_SelectedIndexChanged({
            if ($clbCols.SelectedItem) {
                $name = $clbCols.SelectedItem.ToString()
                if ($grid.Columns.Contains($name)) {
                    $txtHeader.Text = $grid.Columns[$name].HeaderText
                }
            }
        })

        $btnRename.Add_Click({
            if (-not $clbCols.SelectedItem) { return }
            $name = $clbCols.SelectedItem.ToString()
            $new  = ($txtHeader.Text ?? "").Trim()
            if ($new.Length -eq 0) { return }
            if ($grid.Columns.Contains($name)) {
                $grid.Columns[$name].HeaderText = $new
            }
        })

        $btnExportCsv.Add_Click({
            $dlg = New-Object System.Windows.Forms.SaveFileDialog
            $dlg.Filter = "CSV (*.csv)|*.csv|All Files (*.*)|*.*"
            $dlg.Title  = "Export filtered rows to CSV"
            if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }

            $visibleCols = @()
            foreach ($c in $grid.Columns) { if ($c.Visible) { $visibleCols += $c } }

            $out = foreach ($drv in $view) {
                $o = [ordered]@{}
                foreach ($c in $visibleCols) {
                    $key = if ([string]::IsNullOrWhiteSpace($c.DataPropertyName)) { $c.Name } else { $c.DataPropertyName }
                    $o[$c.HeaderText] = $drv.Row[$key]
                }
                [pscustomobject]$o
            }

            $out | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $dlg.FileName
            [System.Windows.Forms.MessageBox]::Show("Exported CSV:`n$($dlg.FileName)","Export",
                [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null
        })

        $btnExportXlsx.Add_Click({
            $excelType = [Type]::GetTypeFromProgID("Excel.Application")
            if (-not $excelType) {
                [System.Windows.Forms.MessageBox]::Show("Excel is not installed. Use Export CSV instead.","XLSX Export",
                    [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null
                return
            }

            $dlg = New-Object System.Windows.Forms.SaveFileDialog
            $dlg.Filter = "Excel Workbook (*.xlsx)|*.xlsx"
            $dlg.Title  = "Export filtered rows to XLSX (requires Excel)"
            if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }

            $visibleCols = @()
            foreach ($c in $grid.Columns) { if ($c.Visible) { $visibleCols += $c } }

            $xl = $null; $wb = $null; $ws = $null
            try {
                $xl = New-Object -ComObject Excel.Application
                $xl.Visible = $false
                $xl.DisplayAlerts = $false

                $wb = $xl.Workbooks.Add()
                $ws = $wb.Worksheets.Item(1)

                for ($i=0; $i -lt $visibleCols.Count; $i++) {
                    $ws.Cells.Item(1, $i+1).Value2 = $visibleCols[$i].HeaderText
                }

                $r = 2
                foreach ($drv in $view) {
                    for ($i=0; $i -lt $visibleCols.Count; $i++) {
                        $c = $visibleCols[$i]
                        $key = if ([string]::IsNullOrWhiteSpace($c.DataPropertyName)) { $c.Name } else { $c.DataPropertyName }
                        $val = $drv.Row[$key]
                        $ws.Cells.Item($r, $i+1).Value2 = if ($null -eq $val) { "" } else { "$val" }
                    }
                    $r++
                }

                $wb.SaveAs($dlg.FileName, 51) # xlOpenXMLWorkbook
                $wb.Close()
                $xl.Quit()

                [System.Windows.Forms.MessageBox]::Show("Exported XLSX:`n$($dlg.FileName)","Export",
                    [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null
            }
            finally {
                foreach ($o in @($ws,$wb,$xl)) {
                    try { if ($o) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($o) | Out-Null } } catch {}
                }
            }
        })

        if ($chartsAvailable) {
            $btnClearChart.Add_Click({
                $chart.Series.Clear()
                $chart.Titles.Clear()
                $chart.Legends.Clear()
            })

            $btnPlot.Add_Click({
                # Basic validation
                if (-not $cmbX.SelectedItem -or -not $cmbY.SelectedItem) { return }

                if ($view.Count -eq 0) {
                    [System.Windows.Forms.MessageBox]::Show(
                        "No rows available to plot (check your filter).",
                        "Plot",
                        [System.Windows.Forms.MessageBoxButtons]::OK,
                        [System.Windows.Forms.MessageBoxIcon]::Information
                    ) | Out-Null
                    return
                }

                $xCol = $cmbX.SelectedItem.ToString()
                $yCol = $cmbY.SelectedItem.ToString()
                $type = $cmbChartType.SelectedItem.ToString()
                $agg  = $cmbAgg.SelectedItem.ToString()

                $points = @()

                # ---- Build points ----
                if ($agg -eq 'Raw' -and $type -ne 'Pie') {
                    # Raw: plot every numeric row
                    foreach ($drv in $view) {
                        $x  = "$($drv.Row[$xCol])"
                        $ys = "$($drv.Row[$yCol])"
                        $y  = 0.0
                        if ([double]::TryParse($ys, [ref]$y)) {
                            $points += [pscustomobject]@{ X = $x; Y = [double]$y }
                        }
                    }
                }
                else {
                    # Bucket by X and aggregate
                    $buckets = @{}

                    foreach ($drv in $view) {
                        $x  = "$($drv.Row[$xCol])"
                        $ys = "$($drv.Row[$yCol])"

                        if (-not $buckets.ContainsKey($x)) {
                            $buckets[$x] = New-Object System.Collections.Generic.List[double]
                        }

                        if ($agg -eq 'Count') {
                            $buckets[$x].Add(1.0)
                            continue
                        }

                        $y = 0.0
                        if ([double]::TryParse($ys, [ref]$y)) {
                            $buckets[$x].Add($y)
                        }
                    }

                    foreach ($k in $buckets.Keys) {
                        $list = $buckets[$k]
                        if ($list.Count -eq 0) { continue }

                        switch ($agg) {
                            'Avg'   { $val = ($list | Measure-Object -Average).Average }
                            'Count' { $val = $list.Count }
                            default { $val = ($list | Measure-Object -Sum).Sum }  # Sum
                        }

                        $points += [pscustomobject]@{ X = $k; Y = [double]$val }
                    }
                }

                # ---- No data guard ----
                if (-not $points -or $points.Count -eq 0) {
                    [System.Windows.Forms.MessageBox]::Show(
                        "No numeric data available to plot for '$yCol' using aggregation '$agg'.",
                        "Plot",
                        [System.Windows.Forms.MessageBoxButtons]::OK,
                        [System.Windows.Forms.MessageBoxIcon]::Information
                    ) | Out-Null
                    return
                }

                # ---- Sorting / Pie grouping ----
                if ($type -eq 'Pie') {
                    $sorted = $points | Sort-Object Y -Descending
                    # Avoid unreadable pies: group small slices into "Other"
                    if ($sorted.Count -gt 12) {
                        $top  = $sorted | Select-Object -First 11
                        $rest = $sorted | Select-Object -Skip 11
                        $otherSum = ($rest | Measure-Object -Property Y -Sum).Sum
                        $points = @($top) + @([pscustomobject]@{ X = "Other"; Y = [double]$otherSum })
                    }
                    else {
                        $points = $sorted
                    }
                }
                else {
                    $points = $points | Sort-Object X
                }

                # ---- Reset chart ----
                $chart.Series.Clear()
                $chart.Titles.Clear()
                $chart.Legends.Clear()

                # Build title before Add()
                $title = "{0}: {1} by {2} ({3})" -f $type, $yCol, $xCol, $agg
                [void]$chart.Titles.Add($title)

                # Add a legend (especially helpful for Pie)
                [void]$chart.Legends.Add((New-Object System.Windows.Forms.DataVisualization.Charting.Legend("Legend1")))
                $chart.Legends["Legend1"].Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right

                $series = New-Object System.Windows.Forms.DataVisualization.Charting.Series("Series1")
                $series.IsValueShownAsLabel = $true
                $series.Legend = "Legend1"

                switch ($type) {
                    'Bar'   { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Bar }
                    'Line'  { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Line }
                    'Pie'   { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Pie }
                    'Area'  { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Area }
                    default { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Column }
                }

                if ($type -eq 'Pie') {
                    # Better Pie readability
                    $series["PieLabelStyle"] = "Outside"
                    $series["PieLineColor"]  = "Gray"
                    $series.Label = "#VALX: #PERCENT{P0}"
                    $series.LegendText = "#VALX (#VALY)"
                }
                else {
                    $series.Label = "#VALY"
                    $series.LegendText = $yCol
                }

                foreach ($p in $points) {
                    $dpIndex = $series.Points.AddXY($p.X, $p.Y)
                    try { $series.Points[$dpIndex].ToolTip = "{0}: {1}" -f $p.X, $p.Y } catch {}
                }

                [void]$chart.Series.Add($series)
            })
        }

        # Convenience hotkeys
        $form.Add_KeyDown({
            param($s, $e)
            if ($e.Control -and $e.KeyCode -eq 'F') { $txtSearch.Focus(); $e.Handled = $true }
            if ($e.KeyCode -eq 'Escape') { $form.Close(); $e.Handled = $true }
        })

        # Show dialog
        [void]$form.ShowDialog()
    }
}


Export-ModuleMember -Function Show-GridChartUI