0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PowerShell】Windows標準機能でグラフ描画 〜インタラクティブ機能サンプル編〜

0
Posted at

前回、PowerShellで描画したグラフにインタラクティブ機能を付加する方法を紹介しました。

ポイントは HitTestメソッドでしたね。

今回の記事では、実際にHitTestを使用して作成したインタラクティブ機能のサンプルをいくつか紹介します。
前回まで同様、すべてWindows標準機能のみで作成しています。

目次

  1. グラフ領域外にHit要素の情報表示
  2. ホバーによるHit判定
  3. ツールチップ表示(カスタムツールチップ)
  4. Hit要素のハイライトON・OFF
  5. クリックでラベル表示
  6. ドラッグで範囲指定しまとめて処理
  7. Ctrl+ドラッグで範囲指定ズーム(+ズームリセット機能付き)
  8. Ctrl+マウスホイールでカーソル位置を中心にズーム
  9. 右クリックでコンテキストメニューを表示
  10. 凡例クリックで指定系列の強調

おわりに

1️⃣ グラフ領域外にHit要素の情報表示

クリックしたデータ点の詳細情報を、コンソールではなくウィンドウ上の専用領域に表示させるサンプルです。

  • 使用するイベント: Add_MouseClick
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理: クリックされたデータ点の詳細情報を、グラフ右側に設置したLabelコントロールへ表示

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization


# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 1000
    Height = 600
}

# チャートの作成 ================================================

+# 右側に情報表示ラベル用の領域を確保する幅(ピクセル)
+$infoPanelWidth = 240
# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
+    Width  = $form.ClientSize.Width - 100 - $infoPanelWidth
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# 右側・情報表示用ラベルを作成(Label)
+$infoLabel = [System.Windows.Forms.Label] @{
+    AutoSize    = $false
+    Width       = $infoPanelWidth - 20
+    Height      = $chart.Height
+    Left        = $chart.Left + $chart.Width + 20
+    Top         = $chart.Top
*    BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle    # ボーダー線あり(細めの実線)
+    Font        = [System.Drawing.Font]::new("Meiryo UI", 9)
+    BackColor   = [System.Drawing.Color]::FromArgb(255, 255, 255)
+}
+$infoLabel.UseCompatibleTextRendering = $false    # 描画エンジンをGDIベースに指定($trueだと文字が滲む)
+$infoLabel.Text = "データポイントをクリックすると詳細を表示"
+$form.Controls.Add($infoLabel)


# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $series.Points[$i].Tag = $genreData[$i]
    }
}

# イベント設定 ================================================

+# 情報出力するヘッダ列名を定義
+$displayHeaders = @(
+    "ゲームタイトル",
+    "ジャンル",
+    "メタスコア",
+    "ユーザースコア"
+)

# チャートにクリックイベントを追加
$chart.Add_MouseClick({
    param($sender, $e)

    # マウスクリック座標で当たり判定を実行
    $hitTestResult = $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint)

    # 判定結果、もしデータ点がクリックされていたら
    if ($hitTestResult.ChartElementType -eq 'DataPoint') {

+        # クリックされたデータポイントのオブジェクトを取得
+        # 堅牢性のため、結果を@()で囲み、常に配列として扱う
+        $clickedPoints = @($hitTestResult.Object)
+        
+        # 右側ラベルに出力する情報データを作成
+        $lines = @()
+
+        # 複数の点が重なっている場合も考慮し、ループで処理
+        foreach ($point in $clickedPoints) {
+            # オブジェクトのTagに格納しておいた情報を取得
+            $record = $point.Tag
+
+            foreach($header in $displayHeaders) {
+                $lines += "$($header): $($record.$header)"
+            }
+            
+            # 複数の点がクリックされた場合は、間に区切り線を入れる
+            # ただし、最後の点の後には入れない
+            if ($point -ne $clickedPoints[-1]) {
+                $lines += "--------------------------------"
+            }
+        }
+
+        # 右側ラベルに情報を表示
+        $infoLabel.Text = ($lines -join [Environment]::NewLine)
+
+    } else {
+        # 右側ラベルを初期化(データポイントがクリックされていない場合)
+        $infoLabel.Text = "データポイントをクリックすると詳細を表示"
+    }
})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

フォームの拡張

  • ゲーム情報の詳細を表示するLabelコントロールをチャートの右側に配置。

データが重なったポイントでの処理

  • DataPointにヒットしたら、レコードから指定の列を取り出して配列$linesに蓄積していく。複数Pointをヒットした場合、ゲーム間で区切り線を追加させている。
  • ヒットしたすべてのPointを読み込んだら、配列$linesを結合し、Labelを更新。結合に使用している[Environment]::NewLineは環境に依存せず使用できる改行コード。

データポイント以外クリック時の処理追加

  • elseで何も無い箇所をクリックした場合の処理を追加。今回はLabelの内容を初期化するようにした。

2️⃣ ホバーによるHit判定

クリックではなく、データ点にマウスカーソルを合わせる(ホバーする)だけで情報が表示されるようにしたサンプルです。よりインタラクティブ性が増し、直感的なデータ探索が可能になります。

  • 使用するイベント: Add_MouseMove
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理: ホバーされたデータ点の詳細情報を、グラフ右側に設置したLabelコントロールに表示する。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization


# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 1000
    Height = 600
}

# チャートの作成 ================================================

# 右側に情報表示ラベル用の領域を確保する幅(ピクセル)
$infoPanelWidth = 240
# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100 - $infoPanelWidth
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# 右側・情報表示用ラベルを作成(Label)
$infoLabel = [System.Windows.Forms.Label] @{
    AutoSize    = $false
    Width       = $infoPanelWidth - 20
    Height      = $chart.Height
    Left        = $chart.Left + $chart.Width + 20
    Top         = $chart.Top
    BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle    # ボーダー線あり(細めの実線)
    Font        = [System.Drawing.Font]::new("Meiryo UI", 9)
    BackColor   = [System.Drawing.Color]::FromArgb(255, 255, 255)
}
$infoLabel.UseCompatibleTextRendering = $false    # 描画エンジンをGDIベースに指定($trueだと文字が滲む)
$infoLabel.Text = "データポイントをクリックすると詳細を表示"
$form.Controls.Add($infoLabel)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $series.Points[$i].Tag = $genreData[$i]
    }
}

+# 直前にホバーしたデータポイントを保持する変数(チラツキ防止用)
+$script:lastHoveredPoint = $null

# イベント設定 ================================================

+# チャートにMouseMoveイベントを追加(ホバーで情報表示)
+$chart.Add_MouseMove({
+    param($sender, $e)
+
+    # マウスカーソル座標で当たり判定を実行
+    $hitTestResult = $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint)
+
+    
+    # 判定結果、もしデータ点がホバーされていたら
+    if ($hitTestResult.ChartElementType -eq 'DataPoint') {
+        # ホバーされたデータポイントのオブジェクトを取得
+        $hoveredPoints = @($hitTestResult.Object)
+
+        # 直前の点と同じなら、再描画をスキップ(チラツキ防止)
+        if ($hoveredPoints[0] -ne $script:lastHoveredPoint) {
+            $script:lastHoveredPoint = $hoveredPoints[0]

            # 情報出力するヘッダ列名を定義
            $displayHeaders = @(
                "ゲームタイトル",
                "ジャンル",
                "メタスコア",
                "ユーザースコア"
            )
            
            # 右側ラベルに出力する情報データを作成
            $lines = @()
            # 複数の点が重なっている場合も考慮し、ループで処理
+            foreach($point in $hoveredPoints) {
                # オブジェクトのTagに格納しておいた情報を取得
                $record = $point.Tag
                
                foreach($header in $displayHeaders) {
                    $lines += "$($header): $($record.$header)"
                }

                # 複数の点がクリックされた場合は、間に区切り線を入れる
+                if($point -ne $hoveredPoints[-1]) {
                    $lines += "--------------------------------"
                }
            }
            # 右側ラベルに情報を表示
            $infoLabel.Text = ($lines -join [Environment]::NewLine)
        }
    } else {
+        # データ点からカーソルが外れた場合、ラベルを初期化
+        if ($script:lastHoveredPoint -ne $null) {
+            $script:lastHoveredPoint = $null
+            $infoLabel.Text = "データポイントにカーソルを合わせると詳細を表示"
        }
    }
})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

イベントの変更:

  • Add_MouseClickAdd_MouseMoveに変更。これにより、マウスが1ピクセル動くたびにイベントが発生し、HitTestが実行される。

チラツキ防止の仕組み:

  • MouseMoveは非常に高頻度で発生するため、そのままラベルを更新すると表示がチラついたり、無駄な処理で重くなりがち。
  • そこで、$script:lastHoveredPointに「直前にホバーしていたデータ点」を記録。(次のイベント発生時の処理の際共有が必要かつ書き換えを行なうため$Script:が必要)
  • HitTestの結果、現在カーソルが乗っている点と$script:lastHoveredPointが同じであれば、ラベルの更新処理をスキップ。
  • これにより、同じデータ点の上でカーソルが細かく動いている間の、不要な再描画を抑制する。

カーソルが外れた際の処理:

  • データ点以外の場所にカーソルが移動した場合(elseブロック)、$script:lastHoveredPoint$nullでないことを確認してから、ラベルの表示を初期化し、$script:lastHoveredPoint$nullに戻す。
  • このif文があることで、「データ点から離れた直後の1回だけ」この初期化処理が実行されるため、何もない領域でカーソルを動かしている間、無駄な処理が走り続けるのを防ぐ。

3️⃣ ツールチップ表示(カスタムツールチップ)

ホバーされたデータ点の情報をツールチップで表示するサンプルです。
応答速度などをカスタマイズしたいので、自身でコンポーネントを作成し、HitTestと組み合わせています。

  • 使用するイベント: Add_MouseMove
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理: ホバーされたデータ点の詳細情報を、カスタマイズしたToolTipコンポーネントで表示する。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization


# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

+# ToolTipコンポーネントを作成
+$toolTip = [System.Windows.Forms.ToolTip]::new()
+# 表示遅延を100ミリ秒(0.1秒)に設定(デフォルトは500ms)
+$toolTip.InitialDelay = 100
+# 見た目をカスタマイズ
+$toolTip.BackColor = [System.Drawing.Color]::FromArgb(240, 255, 200, 230) # 背景色: 薄い黄色
+$toolTip.ForeColor = [System.Drawing.Color]::FromArgb(40, 40, 40)   # 前景色: 暗い灰色
+$toolTip.ToolTipIcon = [System.Windows.Forms.ToolTipIcon]::Info       # アイコン: 情報
+$toolTip.ToolTipTitle = "ゲーム詳細"                                # タイトル


# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $series.Points[$i].Tag = $genreData[$i]
    }
}

# 直前にホバーしたデータポイントを保持する変数(チラツキ防止用)
$script:lastHoveredPoint = $null    # イベント間で保持するため、スクリプトブロック変数として定義

# イベント設定 ================================================

# チャートにMouseMoveイベントを追加(ホバーで情報表示)
$chart.Add_MouseMove({
    param($sender, $e)

    # マウスカーソル座標で当たり判定を実行 
    $hitTestResult = $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint)
    
    # 判定結果、もしデータ点がホバーされていたら
    if ($hitTestResult.ChartElementType -eq 'DataPoint') {
        # ホバーされたデータポイントのオブジェクトを取得
        # 堅牢性のため、結果を@()で囲み、常に配列として扱う
        $hoveredPoints = @($hitTestResult.Object)

        # 直前の点と同じなら、再描画をスキップ(チラツキ防止)
        # ※ 重複点を考慮し、先頭の点のみを比較対象とする
        if ($hoveredPoints[0] -ne $script:lastHoveredPoint) {
            $script:lastHoveredPoint = $hoveredPoints[0]

            # 情報出力するヘッダ列名を定義
            $displayHeaders = @(
                "ゲームタイトル",
                "ジャンル",
                "メタスコア",
                "ユーザースコア"
            )

            # ツールチップに出力する情報データを作成
            $lines = @()
            # 複数の点が重なっている場合も考慮し、ループで処理
            foreach($point in $hoveredPoints) {
                # オブジェクトのTagに格納しておいた情報を取得
                $record = $point.Tag
                
                foreach($header in $displayHeaders) {
                    $lines += "$($header): $($record.$header)"
                }

                # 複数の点がクリックされた場合は、間に区切り線を入れる
                # ただし、最後の点の後には入れない
                if($point -ne $hoveredPoints[-1]) {
                    $lines += "--------------------------------"
                }
            }

+            # ToolTipにテキストを設定
+            $toolTip.SetToolTip($sender, ($lines -join [Environment]::NewLine))
+            
        }
    } else {
        # データ点からカーソルが外れた場合
        # $script:lastHoveredPointをチェックすることで、データ点から離れた直後というイベントの変わり目のみを捉え、不要な処理を省く
        if ($script:lastHoveredPoint -ne $null) {
            $script:lastHoveredPoint = $null
+            # ToolTipを空に設定して非表示にする
+            $toolTip.SetToolTip($sender, "")
        }
    }
})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

ToolTipコンポーネントの作成と設定:

  • [System.Windows.Forms.ToolTip]インスタンスを作成し、背景色、文字色、アイコン、タイトルなどを設定。
  • InitialDelayプロパティで、表示されるまでの待ち時間を調整可。

ツールチップの表示:

  • $toolTip.SetToolTip()メソッドの第2引数に表示する文字列を渡すだけ。第1引数にはツールチップを表示したいコントロール(今回は$chart)を指定。
  • 表示位置は自動で設定されるため指定不要。
  • 非表示にする場合は第2引数に空文字列("")を渡す。

4️⃣ Hit要素のハイライトON・OFF

データ点をクリックすると、その点をハイライトする(マーカーサイズや枠線を変更する)機能です。再度クリックすると元のスタイルに戻るトグル動作を実装します。

  • 使用するイベント: Add_MouseClick
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理: クリックされたデータ点のマーカースタイル(サイズ、枠線の色・太さ)を変更する。再度クリックされると元のスタイルに戻る。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization


# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)


    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $point = $series.Points[$i]
+        # Tagプロパティに、元のスタイル、ハイライト状態を格納するカスタムオブジェクトを設定
+        $point.Tag = [PSCustomObject]@{
+            IsHighlighted       = $false    # ハイライト状態を記録するフラグ
+            # 元のマーカースタイル情報(解除時に復元するため使用)
+            OriginalMarkerSize  = $series.MarkerSize
+            OriginalMarkerBorderColor = $series.MarkerBorderColor
+            OriginalMarkerBorderWidth = $series.MarkerBorderWidth
        }
    }
}

# イベント設定 ================================================

+# ハイライト時のスタイル定義
+$highlightedMarkerSize = 12
+$highlightedBorderColor = [System.Drawing.Color]::FromArgb(255, 227, 60, 42) # 強い赤色
+$highlightedBorderWidth = 3

# チャートにクリックイベントを追加
$chart.Add_MouseClick({
    param($sender, $e)

    # マウスクリック座標で当たり判定を実行し、常に配列として受け取る
    $hitResults = @( $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint) )
  
+    # ヒットした全てのデータポイントをループし、各種トグル処理を行う
+    foreach ($p in $hitResults.Object) {
+
+        # ハイライト状態をトグル
+        if ($p.Tag.IsHighlighted) {
+            # ハイライト解除 (Tagに保存した元のスタイルに戻す)
+            $p.MarkerSize = $p.Tag.OriginalMarkerSize
+            $p.MarkerBorderColor = $p.Tag.OriginalMarkerBorderColor
+            $p.MarkerBorderWidth = $p.Tag.OriginalMarkerBorderWidth
+            $p.Tag.IsHighlighted = $false
+        } else {
+            # ハイライト設定
+            $p.MarkerSize = $highlightedMarkerSize
+            $p.MarkerBorderColor = $highlightedBorderColor
+            $p.MarkerBorderWidth = $highlightedBorderWidth
+            $p.Tag.IsHighlighted = $true
+        }
+    }    
})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

Tagプロパティによるフラグ管理と元スタイルの保持:

  • 各データポイントのTagプロパティに、複数の情報を格納できる PSCustomObject を設定。
  • この中に、ハイライト有無の管理用フラグIsHighlighted)と、ハイライト解除時に必要な元のスタイルを保存する。
  • 元のスタイルに戻す方法として、都度Pointの親Seriesを参照する方法もあるが、親Seriesを参照するには全データを検索せねばならず処理が重くなる。そのため、Tagに記録するほうが効率的。

クリックイベントでのハイライト状態のトグル処理:

  • データ点がクリックされた際に、TagIsHighlightedフラグを判定し、スタイルおよびフラグを切り替える。

5️⃣ クリックでラベル表示

.NETのチャートにはラベル表示の機能も備わっています。
そこで、クリックしたデータ点のラベル(ここではゲームタイトル)の表示・非表示を切り替える機能を追加してみます。
ここではサンプル4のハイライトに追加する形で実装してみました。

.NETチャートのラベルは配置が自動で決まり、密集すると見えづらくなったり、勝手に省略されたりするのがイマイチ)
(あと、今回は実装してないですが、まとめてラベル表示を解除できるリセット機能があったほうが良さげです)

  • 使用するイベント: Add_MouseClick
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理: クリックされたデータ点のハイライト状態と、ラベルの表示状態を同時に切り替える。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization


# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)

+    # 系列全体のラベルをデフォルトで非表示にする(個別の設定を優先させるため)
+    $series.Label = ""

    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $point = $series.Points[$i]
        # Tagプロパティに、元のスタイル、ハイライト状態を格納するカスタムオブジェクトを設定
        $point.Tag = [PSCustomObject]@{
+            Record              = $genreData[$i] # ラベル表示用に元レコードを格納
            IsHighlighted       = $false    # ハイライト状態を記録するフラグ
            # 元のマーカースタイル情報(解除時に復元するため使用)
            OriginalMarkerSize  = $series.MarkerSize
            OriginalMarkerBorderColor = $series.MarkerBorderColor
            OriginalMarkerBorderWidth = $series.MarkerBorderWidth

        }

    }
}

# イベント設定 ================================================

# ハイライト時のスタイル定義
$highlightedMarkerSize = 12
$highlightedBorderColor = [System.Drawing.Color]::FromArgb(255, 227, 60, 42) # 強い赤色
$highlightedBorderWidth = 3

# チャートにクリックイベントを追加
$chart.Add_MouseClick({
    param($sender, $e)

    # マウスクリック座標で当たり判定を実行し、常に配列として受け取る
    $hitResults = @( $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint) )
  

    # ヒットした全てのデータポイントをループし、各種トグル処理を行う
    foreach ($p in $hitResults.Object) {

+        # ラベルの表示/非表示をトグル
+        if ([string]::IsNullOrEmpty($p.Label)) {
+            $p.Label = $p.Tag.Record.ゲームタイトル
+        } else {
+            $p.Label = ""
+        }

        # ハイライト状態をトグル
        if ($p.Tag.IsHighlighted) {
            # ハイライト解除 (Tagに保存した元のスタイルに戻す)
            $p.MarkerSize = $p.Tag.OriginalMarkerSize
            $p.MarkerBorderColor = $p.Tag.OriginalMarkerBorderColor
            $p.MarkerBorderWidth = $p.Tag.OriginalMarkerBorderWidth
            $p.Tag.IsHighlighted = $false
        } else {
            # ハイライト設定
            $p.MarkerSize = $highlightedMarkerSize
            $p.MarkerBorderColor = $highlightedBorderColor
            $p.MarkerBorderWidth = $highlightedBorderWidth
            $p.Tag.IsHighlighted = $true
        }
    }    

})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

Tagプロパティへの情報集約:

  • ハイライト機能に必要な情報(状態フラグ、元のスタイル)に加え、ラベル表示機能に必要な元のレコード情報Tagプロパティに集約する。

ラベルを表示させる:

  • DataPointLabelプロパティに文字列を指定することで、個々のデータ点にラベルを表示させられる。
  • 非表示にしたい場合は、Labelに空文字""を指定
  • これらの切り替えをクリックイベント内で実施。サンプル4のハイライト切り替えに追加する形で実装している。
  • ただし、個々のDataPointに任意文字列のラベルを表示させるには、Seriesにラベル設定されていないことが必要。なので、Seriesを作成する際に$series.Label = ""のように空文字を指定している。

6️⃣ ドラッグで範囲指定しまとめて処理

.NETのチャートでは図形を描画することも可能です。
以下は、マウスドラッグによる矩形描画→範囲内に含まれるデータ点にまとめて処理を適用させるサンプルです。今回は例として、サンプル4のハイライト処理を範囲適用させています(サンプル5のラベル表示はなし)

  • 使用するイベント: マウスドラッグ関連(Add_MouseDown, Add_MouseMove, Add_MouseUp
  • HitTestの対象: ChartAreaの任意点
  • ヒット時の処理:
    1. MouseDown: ドラッグ開始。既存のハイライトをリセットし、矩形の描画を開始。
    2. MouseMove: ドラッグ中のマウスの動きに追従して矩形を伸縮させる。
    3. MouseUp: ドラッグ終了。矩形の範囲に含まれる全てのデータ点をハイライトし、矩形を削除する。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定 (PointチャートなのでMarkerBorder系プロパティを使用)
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、状態管理用のカスタムオブジェクトを格納
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $point = $series.Points[$i]
        # なぜカスタムオブジェクト?: ハイライト状態や元のスタイルなど、複数の情報をまとめて管理するため。
        $point.Tag = [PSCustomObject]@{
            Record                    = $genreData[$i] # 元のデータ行
            IsHighlighted             = $false         # ハイライト状態のフラグ
            OriginalMarkerSize        = $series.MarkerSize
            OriginalMarkerBorderColor = $series.MarkerBorderColor
            OriginalMarkerBorderWidth = $series.MarkerBorderWidth
        }
    }
}

# グラフ全体の設定 ================================================

+# ドラッグによる矩形選択のための状態管理変数
+# なぜピクセルで管理?:値で管理すると、描画エリアとコントロールサイズの違いから「比率のズレ」が生じる。
+# ピクセルで描画し、最後に値に変換することで、見た目と操作感を一致させる。
+$script:selectionStartPixel = $null # ドラッグ開始座標(ピクセル)
+$script:selectionRectangle = $null # 描画する矩形オブジェクト(Annotation)


# イベント設定 ================================================

+# --- ドラッグによる矩形選択イベント ---
+
+# マウスボタンが押されたときのイベント (ドラッグ開始)
+$chart.Add_MouseDown({
+    param($sender, $e)
+
+    # チャートエリア内でのクリックか確認
+    $hitTestResult = $sender.HitTest($e.X, $e.Y)
+    if ($hitTestResult.ChartArea -ne $null) {
+
+        # ドラッグ開始時に、既存のハイライトを全てリセットする
+        foreach ($s in $sender.Series) {
+            foreach ($p in $s.Points) {
+                if ($p.Tag.IsHighlighted) {
+                    $p.MarkerSize        = $p.Tag.OriginalMarkerSize
+                    $p.MarkerBorderColor = $p.Tag.OriginalMarkerBorderColor
+                    $p.MarkerBorderWidth = $p.Tag.OriginalMarkerBorderWidth
+                    $p.Tag.IsHighlighted = $false
+                }
+            }
+        }
+
+
+        # ドラッグ開始点のピクセル座標を記録
+        $script:selectionStartPixel = $e.Location
+
+        # (もしあれば)古い選択矩形を削除
+        if ($script:selectionRectangle -ne $null) {
+            $sender.Annotations.Remove($script:selectionRectangle)
+        }
+
+        # 新しい選択矩形(Annotation)を作成
+        $script:selectionRectangle = [System.Windows.Forms.DataVisualization.Charting.RectangleAnnotation]::new()
+        # なぜTrue?:座標やサイズをグラフの値ではなく、コントロール全体に対する「パーセンテージ」で指定するため。
+        # これにより、マウスのピクセル移動量と見た目の矩形サイズが完全に一致する。
+        $script:selectionRectangle.IsSizeAlwaysRelative = $true
+        # 描画がグラフエリア外にはみ出さないように設定
+        $script:selectionRectangle.ClipToChartArea = $chartArea.Name
+        
+        # 矩形のスタイルを設定
+        $script:selectionRectangle.LineColor = [System.Drawing.Color]::Black
+        $script:selectionRectangle.LineDashStyle = 'Dash'
+        $script:selectionRectangle.BackColor = [System.Drawing.Color]::FromArgb(50, [System.Drawing.Color]::Black)
+        
+        # 矩形の初期位置とサイズを%で設定
+        $cw = [double]$sender.ClientSize.Width
+        $ch = [double]$sender.ClientSize.Height
+        $script:selectionRectangle.X = 100.0 * $e.X / $cw
+        $script:selectionRectangle.Y = 100.0 * $e.Y / $ch
+        $script:selectionRectangle.Width = 0  # 開始時点なので幅0
+        $script:selectionRectangle.Height = 0 # 開始時点なので高さ0
+
+        # チャートに矩形を追加して描画
+        $sender.Annotations.Add($script:selectionRectangle)
+    }
+})

+# マウスが移動したときのイベント (ドラッグ中)
+$chart.Add_MouseMove({
+    param($sender, $e)
+
+    # ドラッグ中(MouseDownイベント後)かを確認
+    if ($script:selectionStartPixel -ne $null) {
+        # チャート全体の幅と高さを取得(%計算の分母として使用)
+        $cw = [double]$sender.ClientSize.Width
+        $ch = [double]$sender.ClientSize.Height
+
+        # ドラッグ開始点と現在点のピクセル座標を取得
+        $startPxX = $script:selectionStartPixel.X
+        $startPxY = $script:selectionStartPixel.Y
+        $curPxX   = $e.X
+        $curPxY   = $e.Y
+
+        # 矩形の左上隅のピクセル座標と、幅・高さをピクセルで計算
+        $minPxX = [math]::Min($startPxX, $curPxX)
+        $minPxY = [math]::Min($startPxY, $curPxY)
+        $wPx    = [math]::Abs($curPxX - $startPxX)
+        $hPx    = [math]::Abs($curPxY - $startPxY)
+
+        # 計算したピクセル値を、チャート全体に対するパーセンテージに変換して矩形を更新
+        # これにより、マウスの動きに追従して矩形が伸縮する
+        $script:selectionRectangle.X = 100.0 * $minPxX / $cw
+        $script:selectionRectangle.Y = 100.0 * $minPxY / $ch
+        $script:selectionRectangle.Width  = 100.0 * $wPx / $cw
+        $script:selectionRectangle.Height = 100.0 * $hPx / $ch
+    }
+})

+# マウスボタンが離されたときのイベント (ドラッグ終了)
+$chart.Add_MouseUp({
+    param($sender, $e)
+
+    # ドラッグ中だったかを確認
+    if ($script:selectionStartPixel -ne $null) {
+        
+        # --- ここからが判定ロジック ---
+        # 最終的な選択範囲のピクセル座標を確定
+        $startPxX = $script:selectionStartPixel.X
+        $startPxY = $script:selectionStartPixel.Y
+        $endPxX   = $e.X
+        $endPxY   = $e.Y
+
+        # どの方向にドラッグしてもいいように、左上(min)と右下(max)のピクセル座標を計算
+        $minPxX = [math]::Min($startPxX, $endPxX)
+        $maxPxX = [math]::Max($startPxX, $endPxX)
+        $minPxY = [math]::Min($startPxY, $endPxY)
+        $maxPxY = [math]::Max($startPxY, $endPxY)
+
+        # 軸オブジェクトを取得(主に可読性のため)
+        $axisX = $chartArea.AxisX
+        $axisY = $chartArea.AxisY
+              
+        # ピクセル値を値に変換
+        #(矩形の描画はピクセル情報をもとに行なったが、データ点の判定には「値」が必要なため)
+        $minX = $axisX.PixelPositionToValue($minPxX)
+        $maxX = $axisX.PixelPositionToValue($maxPxX)
+        $minY = $axisY.PixelPositionToValue($maxPxY) # Y軸はピクセルと値の上下が逆なため、maxピクセルがmin値
+        $maxY = $axisY.PixelPositionToValue($minPxY) # Y軸はピクセルと値の上下が逆なため、minピクセルがmax値
+
+        # 全ての系列、全てのデータポイントをチェック
+        foreach ($s in $sender.Series) {
+            foreach ($p in $s.Points) {
+                # データポイントの値が、変換した値の範囲内に収まっているか判定
+                if (($p.XValue -ge $minX) -and ($p.XValue -le $maxX) -and `
+                    ($p.YValues[0] -ge $minY) -and ($p.YValues[0] -le $maxY)) {
+                    
+                    # 範囲内ならハイライトする (まだハイライトされていなければ)
+                    if (-not $p.Tag.IsHighlighted) {
+                        $p.MarkerSize = 12
+                        $p.MarkerBorderColor = [System.Drawing.Color]::FromArgb(255, 227, 60, 42)
+                        $p.MarkerBorderWidth = 3
+                        $p.Tag.IsHighlighted = $true
+                    }
+                }
+            }
+        }
+
+        # 描画に使った矩形を削除
+        if ($script:selectionRectangle -ne $null) {
+            $sender.Annotations.Remove($script:selectionRectangle)
+            $script:selectionRectangle = $null
+        }
+        # 次のドラッグ操作に備えて、開始点情報をリセット
+        $script:selectionStartPixel = $null
+    }
+})



# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

矩形の「描画」ロジック:

  • グラフに図形を描画するには、チャートの持つAnnotation(=注釈)機能を使用する。ここで使用する矩形以外に、文字や線、矢印、円形、画像などが描画可能
  • 矩形の描画には[(略).Charting.RectangleAnnotation]オブジェクトを使用する。
  • $chart.Annotations.Add(Annotationオブジェクト)で図形が描画される。
  • 描画設定の中で、矩形の開始位置(X, Y)・幅・高さを指定する。これらの指定はChart全体を100(%)とした0~100の比率を示す数値で行なう必要がある。
    (今回明示的に指定しているIsSizeAlwaysRelativeを$falseにするとデータ(軸の値)から指定することも可能)
  • 一方、イベントから得られる座標情報はピクセル。そのため、チャート全体のサイズ(実際にはClientSize)で除算し、比率を取得して指定している。

マウスドラッグに追従して矩形を伸縮させる:

  • Add_MouseDownでマウスクリック開始時、Add_MouseMoveでマウス移動時、Add_MouseUpでマウスクリック終了時に発生するイベントを追加。さらに、MouseDownイベントで作成したAnnotationの有無+MouseMoveでマウスドラッグを判定。
  • MouseDown時とMouseMove時の座標の差分で、矩形の幅と高さを規定。この差分はピクセル値のため、描画ロジックと同様、チャート全体で割って比率に変換する。
  • Annotationは常に左上を起点とし、右下方向への幅・高さを指定する。そのため開始点から上方向や左方向にドラッグするケースに対応する必要がある。
    • 起点は開始点と現在地から Min関数で常に矩形の左上を取得 ・指定できるようにしている。
    • 幅・高さは ABS関数で座標差分の絶対値 を取得し、指定している。

データ点の「判定」ロジック:

  • ハイライト対象を判定するには、マウスイベントから得られたピクセル値の座標を実際のグラフ上の数値に変換する必要がある。
  • そこで、チャートエリアの軸が持つPixelPositionToValue()メソッドを使い、ピクセル座標を実際の数値に変換する。
  • Y軸ではピクセル座標(上から下へ増加)とグラフの値(下から上へ増加)の向きが逆なため注意が必要。ここでは、ピクセル値から数値データに変換する際、Yのみmin値とmax値を入れ替えている。

矩形・ハイライトのリセット処理:

  • 新しい範囲選択を開始するたびに、前の選択結果(ハイライト)を自動クリア。
  • 矩形は範囲選択終了時に都度削除。描画した図形を削除するには$chart.Annotations.Removeメソッドを使用する。
  • オブジェクトを格納した変数も$nullでクリアしている。これはAnnotation有無を図形描画中(ドラッグ中)の判定にしているのが主な理由だが、メモリ節約も兼ねている。

7️⃣ Ctrl+ドラッグで範囲指定ズーム(+ズームリセット機能付き)

1つ前の範囲選択を応用し、範囲指定した領域を拡大表示(ズーム) する機能です。今回はCtrlキーを押しながらドラッグした場合のみ動作するようにしてみました。
加えて、ズームリセット機能 (何もない所をダブルクリック)も追加しています。

  • 使用するイベント: Add_MouseDown, Add_MouseMove, Add_MouseUp, Add_DoubleClick
  • HitTestの対象: ChartAreaの任意点
  • ヒット時の処理:
    1. MouseDown: Ctrlキーが押されている場合のみ、矩形の描画を開始。
    2. MouseMove: ドラッグ中のマウスの動きに追従して矩形を伸縮させる。
    3. MouseUp: ドラッグ終了。矩形の範囲を新しい表示範囲として軸をズームし、矩形を削除する。
    4. DoubleClick: ズームをリセットして全体表示に戻す。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定 (PointチャートなのでMarkerBorder系プロパティを使用)
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $point = $series.Points[$i]
        $point.Tag = [PSCustomObject]@{
            Record = $genreData[$i] # 元のデータ行
        }
    }
}

# グラフ全体の設定 ================================================

# ドラッグによる矩形選択のための状態管理変数
# なぜピクセルで管理?:値で管理すると、描画エリアとコントロールサイズの違いから「比率のズレ」が生じる。
# ピクセルで描画し、最後に値に変換することで、見た目と操作感を一致させる。
$script:selectionStartPixel = $null # ドラッグ開始座標(ピクセル)
$script:selectionRectangle = $null # 描画する矩形オブジェクト(Annotation)

# イベント設定 ================================================


# --- ドラッグによる矩形選択イベント ---

# マウスボタンが押されたときのイベント (ドラッグ開始)
$chart.Add_MouseDown({
    param($sender, $e)

    # チャートエリア内でのクリックか確認
    $hitTestResult = $sender.HitTest($e.X, $e.Y)
    if ($hitTestResult.ChartArea -ne $null) {

+        # Ctrlキーが押されているか確認
+        if ([System.Windows.Forms.Control]::ModifierKeys -ne 'Control') {
+            return # 押されていなければ何もしない
+        }

        # ドラッグ開始点のピクセル座標を記録
        $script:selectionStartPixel = $e.Location

        # (もしあれば)古い選択矩形を削除
        if ($script:selectionRectangle -ne $null) {
            $chart.Annotations.Remove($script:selectionRectangle)
        }

        # 新しい選択矩形(Annotation)を作成
        $script:selectionRectangle = [System.Windows.Forms.DataVisualization.Charting.RectangleAnnotation]::new()
        # なぜTrue?:座標やサイズをグラフの値ではなく、コントロール全体に対する「パーセンテージ」で指定するため。
        # これにより、マウスのピクセル移動量と見た目の矩形サイズが完全に一致する。
        $script:selectionRectangle.IsSizeAlwaysRelative = $true
        # 描画がグラフエリア外にはみ出さないように設定
        $script:selectionRectangle.ClipToChartArea = $chartArea.Name
        
        # 矩形のスタイルを設定 (青色に変更)
        $script:selectionRectangle.LineColor = [System.Drawing.Color]::DodgerBlue
        $script:selectionRectangle.LineDashStyle = 'Dash'
        $script:selectionRectangle.BackColor = [System.Drawing.Color]::FromArgb(50, [System.Drawing.Color]::SkyBlue)
        
        # 矩形の初期位置とサイズを%で設定
        $cw = [double]$chart.ClientSize.Width
        $ch = [double]$chart.ClientSize.Height
        $script:selectionRectangle.X = 100.0 * $e.X / $cw
        $script:selectionRectangle.Y = 100.0 * $e.Y / $ch
        $script:selectionRectangle.Width = 0  # 開始時点なので幅0
        $script:selectionRectangle.Height = 0 # 開始時点なので高さ0

        # チャートに矩形を追加して描画
        $chart.Annotations.Add($script:selectionRectangle)
    }
})

# マウスが移動したときのイベント (ドラッグ中)
$chart.Add_MouseMove({
    param($sender, $e)

    # ドラッグ中(MouseDownイベント後)かを確認
    if ($script:selectionStartPixel -ne $null) {
        # チャート全体の幅と高さを取得(%計算の分母として使用)
        $cw = [double]$chart.ClientSize.Width
        $ch = [double]$chart.ClientSize.Height

        # ドラッグ開始点と現在点のピクセル座標を取得
        $startPxX = $script:selectionStartPixel.X
        $startPxY = $script:selectionStartPixel.Y
        $curPxX   = $e.X
        $curPxY   = $e.Y

        # 矩形の左上隅のピクセル座標と、幅・高さをピクセルで計算
        $minPxX = [math]::Min($startPxX, $curPxX)
        $minPxY = [math]::Min($startPxY, $curPxY)
        $wPx    = [math]::Abs($curPxX - $startPxX)
        $hPx    = [math]::Abs($curPxY - $startPxY)

        # 計算したピクセル値を、チャート全体に対するパーセンテージに変換して矩形を更新
        # これにより、マウスの動きに追従して矩形が伸縮する
        $script:selectionRectangle.X = 100.0 * $minPxX / $cw
        $script:selectionRectangle.Y = 100.0 * $minPxY / $ch
        $script:selectionRectangle.Width  = 100.0 * $wPx / $cw
        $script:selectionRectangle.Height = 100.0 * $hPx / $ch
    }
})

# マウスボタンが離されたときのイベント (ドラッグ終了)
$chart.Add_MouseUp({
    param($sender, $e)

    # ドラッグ中だったかを確認
    if ($script:selectionStartPixel -ne $null) {
        
        # --- ここからが判定ロジック ---
        # 最終的な選択範囲のピクセル座標を確定
        $startPxX = $script:selectionStartPixel.X
        $startPxY = $script:selectionStartPixel.Y
        $endPxX   = $e.X
        $endPxY   = $e.Y

        # どの方向にドラッグしてもいいように、左上(min)と右下(max)のピクセル座標を計算
        $minPxX = [math]::Min($startPxX, $endPxX)
        $maxPxX = [math]::Max($startPxX, $endPxX)
        $minPxY = [math]::Min($startPxY, $endPxY)
        $maxPxY = [math]::Max($startPxY, $endPxY)

        # 軸オブジェクトを取得(主に可読性のため)
        $axisX = $chartArea.AxisX
        $axisY = $chartArea.AxisY
                
        # ピクセル値を値に変換
        #(矩形の描画はピクセル情報をもとに行なったが、データ点の判定には「値」が必要なため)
        $minX = $axisX.PixelPositionToValue($minPxX)
        $maxX = $axisX.PixelPositionToValue($maxPxX)
        $minY = $axisY.PixelPositionToValue($maxPxY) # Y軸はピクセルと値の上下が逆なため、maxピクセルがmin値
        $maxY = $axisY.PixelPositionToValue($minPxY) # Y軸はピクセルと値の上下が逆なため、minピクセルがmax値

+        # 選択範囲が有効か(幅や高さが0などになっていないか)を確認
+        if (($maxX - $minX -gt 0) -and ($maxY - $minY -gt 0)) {
+            # 軸の表示範囲を選択された範囲にズームする
+            $chartArea.AxisX.ScaleView.Zoom($minX, $maxX)
+            $chartArea.AxisY.ScaleView.Zoom($minY, $maxY)
+        }

        # 描画に使った矩形を削除
        if ($script:selectionRectangle -ne $null) {
            $chart.Annotations.Remove($script:selectionRectangle)
            $script:selectionRectangle = $null
        }
        # 次のドラッグ操作に備えて、開始点情報をリセット
        $script:selectionStartPixel = $null
    }
})

+# チャートエリアをダブルクリックでズームリセット
+$chart.Add_DoubleClick({
+    param($sender, $e)
+    # ヒットテストでデータ点の上ではないことを確認(お好みで)
+    $hitTestResult = $sender.HitTest($e.X, $e.Y)
+    if ($hitTestResult.ChartElementType -ne 'DataPoint') {
+        $chartArea.AxisX.ScaleView.ZoomReset()
+        $chartArea.AxisY.ScaleView.ZoomReset()
+    }
+})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

MouseDownイベントでのCtrlキー判定:

  • MouseDownイベントの冒頭で、[System.Windows.Forms.Control]::ModifierKeys静的プロパティをチェック。
  • ModifierKeysで判定できるキーの主要なものは以下
    • Control: Ctrlキー
    • Shift: Shiftキー
    • Menu: Altキー
  • 同時押しを判定したい場合は、"Shift, Control"のように", "(カンマ+半角スペース)で連結したものを-eq比較する。このとき連結するキーの文字列は正しい順序(Shift → Control → Menu(Alt))で連結されていないと正しく判定できないので注意。
  • 上記の同時押しは指定されたキー「のみ」が押されている状態を検知する。指定キーを「含む」キーの押下を検知したい場合はビット演算子(-bor-band)を使用する必要があるが詳細は割愛。

MouseUpイベントでのズーム実行:

  • MouseUpで最終的な矩形のピクセル範囲を値に変換するまでは、サンプル6と全く同じ。
  • 変換後の値の範囲を使って、各軸のScaleView.Zoom()メソッドを呼び出し、ズーム表示を実行している。

ダブルクリックによるズームリセット:

  • Add_DoubleClickイベントを追加し、各軸のScaleView.ZoomReset()メソッドを呼び出してズーム状態をリセット。

8️⃣ Ctrl+マウスホイールでカーソル位置を中心にズーム

Ctrlキーを押しながらマウスホイールを回転させることで、マウスカーソルの位置を中心に直感的な拡大・縮小を実現する機能です。

  • 使用するイベント: Add_MouseWheel
  • HitTestの対象: なし(カーソル位置の座標を直接利用)
  • ヒット時の処理:
    • Ctrlキーを押しながらマウスホイールを回転させると、カーソル位置が中心となるようにグラフを拡大・縮小する。
    • DoubleClick: ズームをリセットして全体表示に戻す。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250) # 薄いグレーを背景色に設定
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸ラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $point = $series.Points[$i]
        $point.Tag = [PSCustomObject]@{
            Record = $genreData[$i] # 元のデータ行
        }
    }
}

+# --- ズームリセット機能のための初期状態保存 ---
+# ズームアウト操作で軸の表示範囲が動的に変更されるため、ダブルクリックでリセットするべき「初期状態」を先に保存しておく。
+$chartArea.RecalculateAxesScale() # 現在のデータに基づいて軸のスケールを強制的に計算させる。
+$script:initialAxisState = [PSCustomObject]@{
+    # X軸とY軸の最小値・最大値を保存
+    XMin       = $chartArea.AxisX.Minimum
+    XMax       = $chartArea.AxisX.Maximum
+    YMin       = $chartArea.AxisY.Minimum
+    YMax       = $chartArea.AxisY.Maximum
+    # 軸の範囲が自動設定(NaN)だったか、固定値だったかを保存
+    XMinIsAuto = [double]::IsNaN([double]$chartArea.AxisX.Minimum)
+    XMaxIsAuto = [double]::IsNaN([double]$chartArea.AxisX.Maximum)
+    YMinIsAuto = [double]::IsNaN([double]$chartArea.AxisY.Minimum)
+    YMaxIsAuto = [double]::IsNaN([double]$chartArea.AxisY.Maximum)
+}

# チャートエリアをダブルクリックでズームリセット
$chart.Add_DoubleClick({
    param($sender, $e)
    # ヒットテストを実行し、クリックされた場所がデータポイントの上ではないことを確認する。
    # なぜ必要か: ポイント上のダブルクリックで意図せずズームリセットが発動するのを防ぐため(挙動としてのお好み)。
    $hitTestResult = $sender.HitTest($e.X, $e.Y)
    if ($hitTestResult.ChartElementType -ne 'DataPoint') {
+        # さきに保存しておいた初期の軸状態に復元(ズームアウトで`Axis.Minimum/Maximum`が変更されている可能性があるため)
+        if ($script:initialAxisState.XMinIsAuto) { $chartArea.AxisX.Minimum = [double]::NaN } else { $chartArea.AxisX.Minimum = $script:initialAxisState.XMin }
+        if ($script:initialAxisState.XMaxIsAuto) { $chartArea.AxisX.Maximum = [double]::NaN } else { $chartArea.AxisX.Maximum = $script:initialAxisState.XMax }
+        if ($script:initialAxisState.YMinIsAuto) { $chartArea.AxisY.Minimum = [double]::NaN } else { $chartArea.AxisY.Minimum = $script:initialAxisState.YMin }
+        if ($script:initialAxisState.YMaxIsAuto) { $chartArea.AxisY.Maximum = [double]::NaN } else { $chartArea.AxisY.Maximum = $script:initialAxisState.YMax }
+
+        # ビューを初期状態にリセット
+        $chartArea.AxisX.ScaleView.ZoomReset(0)
+        $chartArea.AxisY.ScaleView.ZoomReset(0)
    }
})

+# --- Ctrl + マウスホイールによるズームイベント ---
+$chart.Add_MouseWheel({
+    param($sender, $e)
+
+    # Ctrlキーが押されているか確認
+    if ([System.Windows.Forms.Control]::ModifierKeys -ne 'Control') {
+        return # 押されていなければ何もしない
+    }
+    
+    # 頻繁にアクセスするため、軸オブジェクトを変数に格納しておく
+    $axisX = $chartArea.AxisX
+    $axisY = $chartArea.AxisY
+
+    # マウスカーソルの画面座標(ピクセル)を、グラフの軸の値に変換する。
+    # なぜ必要か: ズームの中心をマウスカーソル位置にするため、ピクセル位置をグラフ内の実際の値に変換する必要がある。
+    $posX = $axisX.PixelPositionToValue($e.X)
+    $posY = $axisY.PixelPositionToValue($e.Y)
+
+    # 現在表示されている軸の範囲を取得(この表示範囲を基準に、拡大・縮小後の新しい表示範囲を計算するため)
+    $viewMinX = $axisX.ScaleView.ViewMinimum
+    $viewMaxX = $axisX.ScaleView.ViewMaximum
+    $viewMinY = $axisY.ScaleView.ViewMinimum
+    $viewMaxY = $axisY.ScaleView.ViewMaximum
+
+    # ホイールの回転方向に応じて処理を分岐
+    if ($e.Delta -gt 0) { # ズームイン (奥に回転)
+        $zoomFactor = 0.8
+        # 新しい表示範囲を計算(カーソル位置が中心になるように)
+        $newSizeX = ($viewMaxX - $viewMinX) * $zoomFactor
+        $newSizeY = ($viewMaxY - $viewMinY) * $zoomFactor
+        
+        # 新しい表示範囲の最小値・最大値を計算
+        # (カーソル位置(`posX`)と現在の表示範囲の端(`viewMinX`)との距離をズーム率分だけ縮めることで、カーソル位置がズームの中心に留まるようにする)
+        $newMinX = $posX - ($posX - $viewMinX) * $zoomFactor
+        $newMaxX = $newMinX + $newSizeX
+        $newMinY = $posY - ($posY - $viewMinY) * $zoomFactor
+        $newMaxY = $newMinY + $newSizeY
+    } else { # ズームアウト (手前に回転)
+        $zoomFactor = 1.25
+        # 現在の表示範囲を基準に拡大
+        $newSizeX = ($viewMaxX - $viewMinX) * $zoomFactor
+        $newSizeY = ($viewMaxY - $viewMinY) * $zoomFactor
+
+        # 新しい表示範囲の最小値・最大値を計算する(ズームインと同じ理由)
+        $newMinX = $posX - ($posX - $viewMinX) * $zoomFactor
+        $newMaxX = $newMinX + $newSizeX
+        $newMinY = $posY - ($posY - $viewMinY) * $zoomFactor
+        $newMaxY = $newMinY + $newSizeY
+
+        # --- ズームアウト時に元の描画範囲を超えるための処理 ---
+        # 軸の全体範囲(`Minimum`/`Maximum`)を取得(自動設定(NaN)の場合は、現在の表示範囲を基準とする)
+        $axMinX = if ([double]::IsNaN([double]$axisX.Minimum)) { [double]$viewMinX } else { [double]$axisX.Minimum }
+        $axMaxX = if ([double]::IsNaN([double]$axisX.Maximum)) { [double]$viewMaxX } else { [double]$axisX.Maximum }
+        $axMinY = if ([double]::IsNaN([double]$axisY.Minimum)) { [double]$viewMinY } else { [double]$axisY.Minimum }
+        $axMaxY = if ([double]::IsNaN([double]$axisY.Maximum)) { [double]$viewMaxY } else { [double]$axisY.Maximum }
+
+        # 計算した新しい表示範囲が、軸の現在の全体範囲をはみ出すかチェック
+        # (はみ出す場合に軸の全体範囲を拡張しないと、それ以上ズームアウトできなくなるため)
+        if (($newMinX -lt $axMinX) -or ($newMaxX -gt $axMaxX)) {
+            # 軸の全体範囲を、新しい表示範囲を含むように拡張
+            $axisX.Minimum = [math]::Min($newMinX, $axMinX)
+            $axisX.Maximum = [math]::Max($newMaxX, $axMaxX)
+        }
+        if (($newMinY -lt $axMinY) -or ($newMaxY -gt $axMaxY)) {
+            $axisY.Minimum = [math]::Min($newMinY, $axMinY)
+            $axisY.Maximum = [math]::Max($newMaxY, $axMaxY)
+        }
+    }
+
+    # 計算した新しい表示範囲を`ScaleView`に設定し、グラフの表示を更新
+    $axisX.ScaleView.Zoom($newMinX, $newMaxX)
+    $axisY.ScaleView.Zoom($newMinY, $newMaxY)
+})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

MouseWheelイベントでのズームロジック:

  • Add_MouseWheelでマウスホイールイベント追加。イベント引数$eDeltaでホイールの回転方向(正が奥、負が手前)を取得。
  • ズームの中心:各軸の PixelPositionToValue()を使い、マウスカーソルのピクセル座標をグラフ上のデータとしての数値に変換。ここがズームの中心となる。
  • 新しい表示範囲の計算: (1)現在のカーソル位置と (2)現在の表示範囲の端 の間の距離を求め、(3)その距離をズーム率で伸縮させ、(4)伸縮後の距離を現在のカーソル位置から引き戻すことで、新しい表示範囲を算出。これにより、ズーム後もカーソルが指す値は同じ位置に留まる。

ズームアウト時の描画範囲拡張:

  • ScaleView.Zoom()で設定できる表示範囲は、その軸のAxis.MinimumAxis.Maximumプロパティで定義される「全体範囲」を超えることは不可。
  • そこで、ズームアウトの計算結果がこの全体範囲をはみ出す場合に、Axis.MinimumAxis.Maximumの値そのものを、新しい表示範囲を含むように動的に書き換える。
  • これにより、ユーザーは初期状態よりも広い範囲をズームアウトして見ることができるようになる。

「完全な初期状態」へのリセット:

  • 前述のズームアウト範囲拡張を行うと、Axis.Minimum/Maximumが書き換わってしまう。そのため、単にZoomReset()を呼び出すだけでは最初の表示状態には戻らない。
  • 状態保存: グラフを描画した直後、その時点での各軸のMinimum/Maximumの値と、それらが自動設定だったかどうかを、変数 (PSCustomObject。要$Script:宣言) に保存。
    • [double]::IsNaN(...)は非数NaNか否かを判定するメソッド。[double]のメソッドだが、返り値はBoolean
  • 状態復元: DoubleClickイベント内で、ZoomReset()を呼び出すに、まずAxis.Minimum/Maximumの値を保存しておいた初期状態に戻す。初期軸範囲が自動の場合、非数[double]::NaNを指定して自動設定に戻す。
  • これにより、ZoomReset()が参照する「全体範囲」が初期状態に戻るため、完全に元の表示に復元可能。

9️⃣ 右クリックでコンテキストメニューを表示

データ点を右クリックすると、コンテキストメニューを表示する機能です。
今回のサンプルでは、対象データ点の情報をクリップボードにコピーする機能を追加してみました。

(これだけだと、どの点で右クリックされたか分かりづらいので実際に運用するときはラベル表示などと組み合わせたほうが良いと思います)

  • 使用するイベント: Add_MouseClick
  • HitTestの対象: データ点(DataPoint
  • ヒット時の処理:
    • 右クリックされた座標に存在する全てのデータ点を特定する。
    • コンテキストメニューを表示し、メニュー項目をクリックすると、特定した全データ点の情報をクリップボードにコピーする。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count

    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $series.Points[$i].Tag = $genreData[$i]
    }
}

# イベント設定 ================================================

+# --- コンテキストメニューの準備 ---
+
+# 右クリックされたデータポイントの情報を一時的に保持する変数
+# 重複するデータポイントに対応するため、配列として初期化
+$script:clickedRecordsForMenu = @()
+
+# コンテキストメニュー(右クリックメニュー)を作成
+$contextMenu = [System.Windows.Forms.ContextMenuStrip]::new()

+# メニュー項目1: ゲームタイトルをコピー
+$copyTitleMenuItem = [System.Windows.Forms.ToolStripMenuItem]::new("ゲームタイトルをコピー")
+$copyTitleMenuItem.Add_Click({
+    # メニューがクリックされたときの処理
+    if ($script:clickedRecordsForMenu.Count -gt 0) {
+        # 配列内の全レコードからゲームタイトルを抽出して改行で連結
+        $titles = $script:clickedRecordsForMenu | ForEach-Object { $_.ゲームタイトル }
+        [System.Windows.Forms.Clipboard]::SetText(($titles -join [Environment]::NewLine))
+    }
+})
+$contextMenu.Items.Add($copyTitleMenuItem) | Out-Null

+# メニュー項目2: ゲーム情報をコピー
+$copyInfoMenuItem = [System.Windows.Forms.ToolStripMenuItem]::new("ゲーム情報をコピー")
+$copyInfoMenuItem.Add_Click({
+
+    # メニューがクリックされたときの処理
+    if ($script:clickedRecordsForMenu.Count -gt 0) {
+
+        # 連結した情報を格納する配列
+        $stackedInfo = @()
+        # 配列内の全レコードの情報を連結
+        foreach ($record in $script:clickedRecordsForMenu) {
+            $stackedInfo += "ゲームタイトル: $($record.ゲームタイトル)"
+            $stackedInfo += "プラットフォーム: $($record.プラットフォーム)"
+            $stackedInfo += "発売年: $($record.発売年)"
+            $stackedInfo += "ジャンル: $($record.ジャンル)"
+            $stackedInfo += "メタスコア: $($record.メタスコア)"
+            $stackedInfo += "ユーザースコア: $($record.ユーザースコア)"
+            
+            # 最後のレコードでなければ区切り線を追加
+            if ($record -ne $script:clickedRecordsForMenu[-1]) {
+                $stackedInfo += "--------------------"
+            }
+        }
+        
+        # クリップボードに詳細情報を設定
+        [System.Windows.Forms.Clipboard]::SetText($stackedInfo -join [Environment]::NewLine)
+    }
+})
+#コンテキストメニューにアイテムを追加
+$null = $contextMenu.Items.Add($copyInfoMenuItem)


# チャートにクリックイベントを追加
$chart.Add_MouseClick({
    param($sender, $e)

+    # 右クリックの場合のみコンテキストメニューを表示
+    if ($e.Button -eq 'Right') {
        # マウスクリック座標で当たり判定を実行
        $hitTestResult = $sender.HitTest($e.X, $e.Y, $true, [System.Windows.Forms.DataVisualization.Charting.ChartElementType]::DataPoint)

        # データ点がクリックされていた場合
        if ($hitTestResult.ChartElementType -eq 'DataPoint') {

            # HitTest結果を取得
            $clickedPoints = $hitTestResult.Object

+            # クリックされた全データポイントのレコード情報(Tag)を配列に格納
+            # コンテキストメニューに送る直前の受け皿のタイミングで@()で囲んで配列を保証しないと、次のCountでエラーとなる
+            $script:clickedRecordsForMenu = @( $clickedPoints.Tag )
            
+            # --- コンテキストメニューの更新 ---
+            # レコードが見つかった場合のみメニューを表示
+            if ($script:clickedRecordsForMenu.Count -gt 0) {
+
+                # マウスカーソルの位置にコンテキストメニューを表示
+                $contextMenu.Show($sender, $e.Location)
+            }
        }
    }
})

# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

コンテキストメニューのオブジェクト作成

  • [System.Windows.Forms.ContextMenuStrip]: 右クリックメニュー全体の入れ物となるオブジェクト。
  • [System.Windows.Forms.ToolStripMenuItem]: メニューに表示される個々の項目(「ゲームタイトルをコピー」など)を表すオブジェクト。コンテキストメニュー以外にメニューバーのアイテムにも使用される。
  • 作成したToolStripMenuItemオブジェクトを、ContextMenuStripオブジェクトの Items.Add()メソッドで追加していくことで、メニューを組み立てる

各メニューアイテムをクリックしたときのイベント登録

  • ToolStripMenuItemオブジェクトに、Add_Clickでイベント追加。この{}の中に書かれた処理が、そのメニュー項目がクリックされたときに実行される。
  • $script:clickedRecordsForMenuは、チャートのクリックイベント(後述)で事前に格納しておいた、クリック地点にあるデータ点のレコード情報(の配列)。
  • この配列から必要な情報(例:ゲームタイトル)をForEach-Objectで取り出し、[Environment]::NewLine(改行コード)で連結。
  • [System.Windows.Forms.Clipboard]::SetText() で文字列をクリップボードにコピー。

コンテキストメニュー表示イベントの登録

  • $sender(=$chart)にAdd_MouseClickイベントを追加し、その中でまず$e.Button -eq 'Right'を判定して、右クリックか否か確認。
  • 右クリックであればHitTestを実行→クリックされたデータ点のレコード情報を$script:clickedRecordsForMenuに格納。
  • このとき、最終的にクリックしたレコードの受け皿となる$script:clickedRecordsForMenuに値を代入するタイミングで配列化して渡す必要がある。一つ前の$clickedPointsのタイミングでのみ配列化して渡そうとすると、単体のデータポイントをクリックしたときに配列でないデータ(=スカラー値)を取得してしまい、のちのCountメソッドで正常に処理ができない。
  • 最後に $contextMenu.Show($chart, $e.Location)を実行してメニューを表示
    • 第1引数には、メニューを表示する基準となるコントロール(今回は$sender(=$chart))を指定。
    • 第2引数には、表示する位置を指定。$e.Locationにはクリックされた座標が格納されているため、これを渡すことでマウスカーソルの位置にメニューが表示される。

🔟 凡例クリックで指定系列の強調

HitTestでクリック箇所を判定できる対象は、データポイントだけではありません。凡例や軸なども要素として判定可能です。
この例では、凡例から系列をクリックすると、その系列以外が半透明になるサンプルを作成してみました。

  • 使用するイベント: Add_MouseClick
  • HitTestの対象: 凡例の系列(LegendItem
  • ヒット時の処理:
    • クリックされた系列以外を半透明表示にし、クリックされた系列を強調表示する。
    • 凡例の系列以外がクリックされた場合、強調表示を解除する。

完成スクリプト(クリックで展開)
# 読み込むCSVの指定
$csvPath = ".\game_sales_dataset_merged.csv"


# 事前準備 ================================================

# 必要なアセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

# カラーパレットの色定義
$colorPalette = @(
    "#ff8ab3", # 明るめの赤系
    "#ff9975", # 明るめのオレンジ赤
    "#d5ca39", # 明るめの黄緑    
    "#8eda5c", # 明るめの緑
    "#00e397", # 明るめのエメラルドグリーン
    "#00ceff", # 明るめのシアン
    "#c7b5ff", # 明るめのラベンダーブルー
    "#ffa2ff", # 明るめのマゼンタ

    "#ae3600", # 暗めの赤茶  
    "#7a5b00", # 暗めのカーキ
    "#5b6600", # 暗めのオリーブグリーン
    "#007600", # 暗めの深緑
    "#0080a6", # 暗めのティールブルー
    "#0068ff", # 暗めの青
    "#6f03ff", # 暗めの深紫
    "#d000cc" # 暗めのマゼンタ
)

# マーカースタイルの定義(順繰り)
$markerStyles = @(
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Circle,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Diamond,
    [System.Windows.Forms.DataVisualization.Charting.MarkerStyle]::Triangle
)

# CSVファイルからデータを読み込む(UTF-8想定)
$gameData = Import-Csv -Path $csvPath -Encoding UTF8

# フォームの作成(表示用ウィンドウ)
$form = [System.Windows.Forms.Form] @{
    Text   = "メタスコアとユーザースコアの散布図"
    Width  = 800
    Height = 600
}

# チャートの作成 ================================================

# チャートの作成(固定配置)
$chart = [System.Windows.Forms.DataVisualization.Charting.Chart] @{
    Width  = $form.ClientSize.Width - 100
    Height = $form.ClientSize.Height - 100
    Left   = 50
    Top    = 50
}

# タイトルの設定
$title = [System.Windows.Forms.DataVisualization.Charting.Title] @{
    Text = "メタスコア vs ユーザースコア"
    Font = [System.Drawing.Font]::new("Meiryo UI", 14)
}
$chart.Titles.Add($title)

# 凡例の作成
$legend = [System.Windows.Forms.DataVisualization.Charting.Legend] @{
    Name    = "ジャンル別データ"
    Font    = [System.Drawing.Font]::new("Meiryo UI", 10)
    Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right
}
$chart.Legends.Add($legend)

# フォームにチャートを追加
$form.Controls.Add($chart)

# チャートエリアの作成 ================================================

# チャートエリアの作成
$chartArea = [System.Windows.Forms.DataVisualization.Charting.ChartArea]::new()
$chartArea.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
$chart.ChartAreas.Add($chartArea)

# 軸ラベル
$chartArea.AxisX.Title = "メタスコア"
$chartArea.AxisY.Title = "ユーザースコア"
$chartArea.AxisX.Minimum = 65

# 各軸に共通設定
foreach ($axis in $chartArea.Axes) {
    # グリッド線の色、スタイルを設定
    $axis.MajorGrid.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128, 128) # 50%のグレー
    $axis.MajorGrid.LineDashStyle = [System.Windows.Forms.DataVisualization.Charting.ChartDashStyle]::Dash

    # 軸の色をグレーに設定
    $axis.LineColor = [System.Drawing.Color]::FromArgb(128, 128, 128)
    
    # 副軸目盛りは完全に非表示
    $axis.MinorTickMark.Enabled = $false
    
    # 主軸目盛りはFalseにすると軸と数字の距離が近くなりすぎるので透明にして対応
    $axis.MajorTickMark.LineColor = [System.Drawing.Color]::Transparent
    $axis.MajorTickMark.Size = 1

    # 軸目盛りのラベルのフォーマットを小数点第一位まで表示
    $axis.LabelStyle.Format = "F1"    # Fは"Fixed-point" (固定小数点)、1で小数点第一位まで表示

    # 軸タイトルのフォント指定
    $axis.TitleFont = [System.Drawing.Font]::new("Meiryo UI", 11)
    # 軸ラベルのフォント指定
    $axis.LabelStyle.Font = [System.Drawing.Font]::new("Segoe UI", 9)
}

# シリーズの作成 ================================================

# ユニークなジャンルを取得し、名前順にソート
$uniqueGenres = @($gameData | Select-Object -ExpandProperty ジャンル -Unique | Sort-Object)

# カラー/マーカーインデックスの初期化
$colorIndex = 0
$markerStyleIndex = 0

# ジャンル別に異なるシリーズを作成
foreach ($genre in $uniqueGenres) {
    $series = $chart.Series.Add($genre)
    $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Point
    $series.MarkerSize = 8

    # 色の選択と設定(カラーパレットを循環)
    $color = $colorPalette[$colorIndex]
    $baseColor = [System.Drawing.ColorTranslator]::FromHtml($color)
    $series.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)
    $colorIndex = ($colorIndex + 1) % $colorPalette.Count
+    # SeriesのTagプロパティに、アルファ値を含まない基本色を保存しておく
+    $series.Tag = $baseColor
    
    # マーカースタイルの選択と設定(マーカースタイルを循環)
    $series.MarkerStyle = $markerStyles[$markerStyleIndex]
    $markerStyleIndex = ($markerStyleIndex + 1) % $markerStyles.Count
    
    # マーカー枠線の設定
    $series.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0) # 不透明度40%の黒
    $series.MarkerBorderWidth = 1

    # 該当ジャンルのデータを抽出
    $genreData = @( $gameData | Where-Object { $_.ジャンル -eq $genre } )

    # データバインドするためのXY配列を生成
    # Import-Csvで読み込んだデータは文字列型のため、数値型 [double] に変換する
    $xValues = @( $genreData.メタスコア | ForEach-Object { [double]$_ } )  
    $yValues = @( $genreData.ユーザースコア | ForEach-Object { [double]$_ } )

    # データバインド
    $series.Points.DataBindXY($xValues, $yValues)

    
    # 各データポイントのTagプロパティに、対応するデータ行のオブジェクトを格納しておく
    for ($i = 0; $i -lt $genreData.Count; $i++) {
        $series.Points[$i].Tag = $genreData[$i]
    }
    
}

# イベント設定 ================================================

# チャートにクリックイベントを追加
$chart.Add_MouseClick({
    param($sender, $e)

    # マウスクリック座標で当たり判定を実行
    $hitTestResult = $sender.HitTest($e.X, $e.Y)

+    # クリックされた系列名を保持する変数
+    $clickedSeriesName = $null
+
+    # 判定結果、もし凡例項目がクリックされていたら
+    if ($hitTestResult.ChartElementType -eq 'LegendItem') {
+        # LegendItemオブジェクトから対応する系列名を取得
+        $clickedSeriesName = $hitTestResult.Object.SeriesName
+    }
+
+    # すべての系列をループして、強調/非強調を切り替え
+    foreach ($s in $chart.Series) {
+        # Tagに保存しておいた基本色を取得
+        $baseColor = $s.Tag
+
+        # クリックされたシリーズ名とループで参照中のシリーズ名が一致する場合 → 対象を強調表示
+        # 凡例以外がクリックされた場合(シリーズ名は$nullになる) → 全強調表示に戻す
+        if (($s.Name -eq $clickedSeriesName) -or ($null -eq $clickedSeriesName)) {
+            # 強調表示
+            $s.MarkerColor = [System.Drawing.Color]::FromArgb(191, $baseColor.R, $baseColor.G, $baseColor.B)    # 不透明度75%
+            $s.MarkerBorderColor = [System.Drawing.Color]::FromArgb(102, 0, 0, 0)    # 不透明度40%の黒
+        } else {
+            # 非強調(半透明)表示
+            $s.MarkerColor = [System.Drawing.Color]::FromArgb(30, $baseColor.R, $baseColor.G, $baseColor.B)    # 不透明度12%
+            $s.MarkerBorderColor = [System.Drawing.Color]::FromArgb(20, 0, 0, 0)    # 不透明度8%の黒
+        }
+    }
})


# 出力 ================================================

# フォームの表示
$null = $form.ShowDialog()

ポイント✏️

Hit要素がLegendItemか判定

  • 系列がクリックされた場合、HitTestResultChartElementTypeLegendItem となるので、それを判定に用いる
  • 系列の場合、ObjectSeriesが格納されるので、SeriesNameで系列名を取得可能。これを$clickedSeriesNameに格納し、強調表示対象の判定に使用する

Tagによる各Seriesのベースカラー情報保持

  • MarkerColorプロパティを半透明に書き換えてしまうと、再度色を変更しようとしたとき、元の色を参照できない
  • なので、今回もハイライトのサンプル同様、Tagにアルファ値の設定されていない元の色情報を格納しておく
  • 一応、RGBは保持されているので、透明度の再設定だけでもとに戻すことも可能だが、今後の汎用性を鑑みてTagに保持する方式にする

全系列ループ→Hit系列名以外を半透明処理

  • $chart.Seriesコレクションをforeachでループし、すべての系列に対して処理を行なう
  • ループで参照している系列名($s.Name)が$clickedSeriesNameと一致した場合は強調表示

元に戻す処理も実装

  • クリックがなされるたびに$clickedSeriesName$nullで初期化を行なっておく
  • 系列でない場所がクリックされた場合、$clickedSeriesNameへの代入処理がなされないため、$nullのままとなる。
  • 前述のループで、もし$clickedSeriesName$nullの場合は強調表示にするように条件設定しておく。
  • これにより、すべての系列が強調表示される=元に戻すことになる。

おわりに

これまで、複数回にわたって紹介してきた「【PowerShell】Windows標準機能でグラフ描画」シリーズはこれで一区切り。
やはりBokehなどのリッチなグラフ描画ライブラリには敵いませんが、Windows標準機能のみでここまで表現できたら御の字かな、と思います。
(本当はドラッグするとグラフ領域がシフトするやつも作りたかったけど上手くいかず断念)

この記事が誰かの業務改善に役立てば幸いです。


「Windows標準機能でグラフ描画」シリーズ

【PowerShell】Windows標準機能でグラフ描画 〜基本編〜
【PowerShell】Windows標準機能でグラフ描画 〜カスタマイズ編〜
【PowerShell】Windows標準機能でグラフ描画 〜インタラクティブ編〜

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?