はじめに
最近Chartコントロールにハマっている。どうせ標準のものだし、融通の利かないコントロールだろうと当初は高をくくっていたが、使ってみると全くそんなことはなく、かゆいところに手が届く、噛めば噛むほど味が出る奥が深いコントロールなのだ。
さて、このChartコントロールの素晴らしさをお伝えするため、今回ChartコントロールおよびMath.Netを使い、Pythonの機械学習本等によく出てくる"あの"散布図行列を作成してみたい。
作りたいもののイメージ
- N個のパラメータとデータを与えると、合計 N x N 個のグラフを並べて表示する。
- 対角線上には各パラメータのヒストグラムを表示する。
- 対角線以外には、対応する2つのパラメータの散布図を表示
- N x N のグラフが、どのパラメータか分かるよう、パラメータ名のラベルを表示する。
- サイズは固定とし、パラメータが多くなってグラフが大きくなってもスクロールバーでスクロールできるようにする。
工夫した点
- スクロールできるようPanelの中にChartコントロールを配置した。
- 並べて表示するといっても、Chartコントロールは1つとし、ChartAreaを自前で N x N 個配置した。
- 大量のグラフを表示するため、軸の表示はしないようにし、グラフを隙間なく並べて、俯瞰しやすいようにした。
- そのままでは、グラフとグラフの間に境界が表示されないので、Chartコントロールの背景を黒にし、少し隙間がでるように配置することで境界っぽく表示されるようにした。
- パラメータ名のラベルは、ChartAreaを並べる場所を少し開け、グラフィックスオブジェクトで直接背景色およびラベル名を描画した。
ソース
さて、ソースは以下の通りだ。データを準備するところはバッサリカットしているのでそのままでは動かないが、上の工夫を見れば大体やっていることは分かるだろう。
using MathNet.Numerics.Statistics;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace Kimisyo.Forms
{
public partial class ScatterPlotForm : Form
{
public int width = 1500;
public int height = 1500;
public int margin = 20;
int nBuckets = 20;
// パラメータIDのリスト
List<int> parameterIds = new List<int>();
// パラメータ名のリスト
List<String> parameterNames = new List<String>();
// パラメータID毎にデータが格納されたディクショナリ
Dictionary<int, Dictionary<int, Double>> parameterId2ValueMap = new Dictionary<int, Dictionary<int, Double>>();
public ScatterPlotForm()
{
InitializeComponent();
}
private void ScatterPlotForm_Load(object sender, EventArgs e)
{
chart1.BackColor = Color.Black;
chart1.Width = this.width;
chart1.Height = this.height;
chart1.Series.Clear();
chart1.Legends.Clear();
chart1.ChartAreas.Clear();
float margin_rate = ((float)margin / (float)chart1.Width) * 100;
Random random = new System.Random();
for (int i = 0; i < parameterIds.Count; i++)
{
for (int j = 0; j < parameterIds.Count; j++)
{
String name = Convert.ToString(i) + "_" + Convert.ToString(j);
ChartArea ca = new ChartArea(name);
ca.AxisX.Enabled = AxisEnabled.False;
ca.AxisY.Enabled = AxisEnabled.False;
ca.Position.X = margin_rate + ((float)j / (float)parameterIds.Count) * (100 - margin_rate);
ca.Position.Y = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate);
ca.Position.Width = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2;
ca.Position.Height = ((float)1/(float)parameterIds.Count) * (100 - margin_rate) - (float)0.2;
this.chart1.ChartAreas.Add(ca);
Series series = new Series();
series.ChartArea = name;
// ScatterPlotを作成
if (i != j)
{
series.ChartType = SeriesChartType.Point;
series.MarkerColor = Color.Blue;
series.MarkerStyle = MarkerStyle.Circle;
series.MarkerSize = 2;
// データ
int parameterIdX = parameterIds[j];
int parameterIdY = parameterIds[i];
Dictionary<int, double> valueMapX = parameterId2ValueMap[parameterIdX];
Dictionary<int, double> valueMapY = parameterId2ValueMap[parameterIdY];
foreach (int sampleId in valueMapX.Keys)
{
DataPoint dp = new DataPoint(valueMapX[sampleId], valueMapY[sampleId]);
series.Points.Add(dp);
}
}
// ヒストグラムを作成
else
{
series.ChartType = SeriesChartType.Column;
series.Color = Color.Blue;
int parameterId = parameterIds[i];
Dictionary<int, double> valueMap = parameterId2ValueMap[parameterId];
Histogram hist = new Histogram(valueMap.Values, nBuckets, -50, 50);
for (int k = 0; k < nBuckets; k++)
{
double mid = Math.Round((hist[k].UpperBound + hist[k].LowerBound) / 2, 1);
series.Points.Add(new DataPoint(mid, hist[k].Count));
}
}
chart1.Series.Add(series);
}
}
}
/// ラベルを表示する(PostPaintイベント)
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
ChartGraphics cg = e.ChartGraphics;
Graphics g = cg.Graphics;
SolidBrush myBrushWhite = new SolidBrush(Color.White);
g.FillRectangle(myBrushWhite, 0, 0, margin-3, this.height);
g.FillRectangle(myBrushWhite, 0, 0, this.width, margin-3);
for(int i=0; i<parameterIds.Count; i++)
{
float margin_rate = ((float)margin / (float)chart1.Width) * 100;
float x_rate = margin_rate + ((float)i / (float)parameterIds.Count) * (100 - margin_rate);
int x = Convert.ToInt32(this.width * x_rate/100);
SolidBrush myBrushBlack = new SolidBrush(Color.Black);
Font font = new Font("MS UI Gothic", 6);
g.DrawString(parameterNames[i], font, myBrushBlack, new Point(x, 3));
}
// 原点を移動
g.TranslateTransform(0, this.height);
// -90度回転
g.RotateTransform(-90f);
// 文字列を描画
for(int i=0; i<parameterIds.Count; i++)
{
float margin_rate = ((float)margin / (float)chart1.Width) * 100;
float x_rate = ((float)i / (float)parameterIds.Count) * (100 - margin_rate);
int x = Convert.ToInt32(this.width * x_rate/100);
SolidBrush myBrushBlack = new SolidBrush(Color.Black);
Font font = new Font("MS UI Gothic", 6);
g.DrawString(parameterNames[parameterIds.Count-i-1], font, myBrushBlack, new Point(x, 3));
}
// 元に戻す
g.ResetTransform();
}
}
}
出来上がり図
以下は1500x1500のサイズに20個のパラメータを並べてみた図である。表示に数秒またされるが、表示後のスクロールは快適である。
図は小さいが、気になるグラフをクリックすると、詳細なグラフを表示する等の処理を組み込めば全く問題ないだろう。
課題
課題としては、グラフの境界線の上にプロットされているように見えてしまい見ずらい。また、X軸、Y軸の範囲が統一されていないなどの問題がある。まだ、Chartコントロールの使い方の理解が不十分なため、今後様々なカスタマイズに挑戦してみたい。