はじめに
C#のChartコントロールで描いた散布図において、目盛間隔を自動調整する方法です。横軸が日時の場合は、Chartコントロール自身が自動で目盛間隔を調整してくれます。同様のことを通常の数値目盛でも行う方法です。
概要
大まかな流れとしては、
-
大体どのくらいの間隔(ピクセル)で目盛を振るかを設定する。例えば75ピクセル毎。
-
現在のグラフエリアの幅(ピクセル)からすると何分割して目盛を振るかを計算する。例えば幅を226として226/75=3.01。
-
予め設定した使用可能な分割数(例えば1,2,4,5,10,20分割)の中から分割数を選択し、決定する。例えば3.01であれば、3以上の最小値である4とする。
-
分割数が決まったら目盛間隔を仮設定する。例えば表示が49~251で4分割であれば(251-49)/4で50.5。
-
計算結果を指数表記にした上で仮数部を取得する。50.5 → 5.05E+1 → 5.05。
-
分割数と同様に予め設定した使用可能な目盛間隔(例えば1,2,5,10)の中から目盛間隔を選択し、決定する。仮数部が5.05であれば、5.05以上の最小値である10とする。
-
指数部より桁を合わせる。10E+1 → 100。
-
目盛間隔が100になるように目盛のオフセットを調整する。例えば0,100,200と振りたいのであれば最小値49にオフセット-49を設定する。
ソースコード
軸の最小値または最大値がDouble.NaN(デフォルト値)の場合は、目盛の自動調整をする前に(autoIntervalAdjust関数を呼ぶ前に)グラフ エリアのプロパティを再計算ChartArea.RecalculateAxesScale();
しておく必要があります。
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;//グラフ用
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
//初期化
chart1.ChartAreas.Clear();
chart1.Series.Clear();
chart1.Legends.Clear();
chart1.Dock = DockStyle.Fill;
//ChartAreaの追加
chart1.ChartAreas.Add(new ChartArea());
//ChartAreaの設定
ChartArea chartArea = chart1.ChartAreas[0];
//移動時にグラフエリアのサイズが
//自動調整(変更)されないようにグラフエリアを固定
chartArea.InnerPlotPosition.Auto = false;
chartArea.InnerPlotPosition.X = 8.0f;
chartArea.InnerPlotPosition.Y = 4.0f;
chartArea.InnerPlotPosition.Width = 84.0f;
chartArea.InnerPlotPosition.Height = 84.0f;
//イベント登録
//最初にグラフを描くときの初期化用
chart1.Paint += new PaintEventHandler(this.chart1_Paint);
//サイズ、スケール変更時
chart1.AxisViewChanged += new EventHandler<ViewEventArgs>(this.chart1_AxisViewChanged);
chart1.SizeChanged += new EventHandler(this.chart1_SizeChanged);
//系列の作成
Series series = new Series
{
Color = Color.Blue,
MarkerColor = Color.Blue,
MarkerSize = 10,
MarkerStyle = MarkerStyle.Circle,
ChartType = SeriesChartType.Line
};
//系列にプロットデータ追加
for (double i = 0; i < 720; i++)
{
series.Points.AddXY(i, Math.Sin(i / 360 * 3.14 * 2));
}
//系列を追加
chart1.Series.Add(series);
//X軸の表示範囲拡大
chart1.ChartAreas[0].AxisX.ScaleView.Zoom(50, 250);
}
//目盛間隔の自動調整
private void autoIntervalAdjust(Axis axis)
{
//軸の最大最小値がDouble.NaNの場合は、
//先にグラフ エリアのプロパティを再計算 ChartArea.RecalculateAxesScale(); しておくこと
//大体何ピクセルごとに目盛をふるか
double pixelWidthInterval = 75.0;
//軸の分割数の配列(この中の値のいずれかで分割される)
int[] splitCounts = new int[] { 1, 2, 4, 5, 10, 20, 40, 50, 100 };
//目盛数値の区切り配列(この中の値のいずれか×10^nが目盛数値になる)
int[] IntervalSteps = new int[] { 1, 2, 5, 10 };
try
{
//グラフビューの最小値、最大値
double vmin = axis.ScaleView.ViewMinimum;
double vmax = axis.ScaleView.ViewMaximum;
//グラフビューの幅(ピクセル)
double pixelWidth = Math.Abs(axis.ValueToPixelPosition(vmax) - axis.ValueToPixelPosition(vmin));
//軸の分割数を仮設定
int splitCount = (int)Math.Floor(pixelWidth / pixelWidthInterval);//仮分割数
//軸の分割数を設定(軸の分割数の配列の中から採用)
int idx;
idx = splitCounts.Length - 1;
for (int i = 0; i < splitCounts.Length; i++)
{
if (splitCount <= splitCounts[i])
{
idx = i;
break;
}
}
splitCount = splitCounts[idx];//分割数(整数)
//目盛間隔を仮設定
double autoInterval = (vmax - vmin) / splitCount; //仮目盛間隔(実数)
double exponent = Math.Floor(Math.Log10(autoInterval)); //仮目盛間隔の指数表記の指数部
double mantissa = autoInterval / Math.Pow(10.0, exponent);//仮目盛間隔の指数表記の仮数部
//目盛間隔を設定(目盛数値の区切り配列の中から採用)
idx = IntervalSteps.Length - 1;
for (int i = 0; i < IntervalSteps.Length; i++)
{
if (mantissa <= IntervalSteps[i])
{
idx = i;
break;
}
}
axis.Interval = IntervalSteps[idx] * Math.Pow(10.0, exponent);//目盛間隔(整数)
//目盛のオフセット、例えば最小値が-10.28で目盛間隔が2の場合は-12から目盛を振る
double left = Math.Floor(vmin / axis.Interval) * axis.Interval;//目盛最左端-12
axis.IntervalOffset = left - vmin;//-12-(-10.28)
}
catch (Exception)
{
;
}
}
//起動時に目盛間隔の自動調整
bool atBoot = true;
private void chart1_Paint(object sender, PaintEventArgs e)
{
if (atBoot)
{
atBoot = false;
autoIntervalAdjust(chart1.ChartAreas[0].AxisX);
autoIntervalAdjust(chart1.ChartAreas[0].AxisY);
}
}
//サイズ変更、ビュー変更時に目盛間隔の自動調整
private void chart1_SizeChanged(object sender, EventArgs e)
{
autoIntervalAdjust(chart1.ChartAreas[0].AxisX);
autoIntervalAdjust(chart1.ChartAreas[0].AxisY);
}
private void chart1_AxisViewChanged(object sender, ViewEventArgs e)
{
autoIntervalAdjust(chart1.ChartAreas[0].AxisX);
autoIntervalAdjust(chart1.ChartAreas[0].AxisY);
}
}
}
補足
日時目盛の書式の自動設定について
横軸が日時の場合、目盛間隔は自動調整されますが、書式は自動調整されません。書式も自動調整したい場合は、例えば下記のように、条件に応じて書式を変更するようにします。
private void autoIntervalAdjust()
{
Axis AxisX = this.chart1.ChartAreas[0].AxisX;
//グラフビューの最小値、最大値、幅
double vmin = AxisX.ScaleView.ViewMinimum;
double vmax = AxisX.ScaleView.ViewMaximum;
double delta = vmax - vmin;
double day = 1.0;
double hour = day / 24.0;
double minute = hour / 60.0;
double second = minute / 60.0;
double msec = second / 1000.0;
double year = 365.0;
//横軸の目盛の書式を変更
if (delta <= 50 * msec)
{
AxisX.LabelStyle.Format = "s.ffff";
}
else if (delta <= 200 * msec)
{
AxisX.LabelStyle.Format = "s.fff";
}
else if (delta <= 5 * second)
{
AxisX.LabelStyle.Format = "m:ss.ff";
}
else if (delta <= 12 * hour)
{
AxisX.LabelStyle.Format = "H:mm:ss";
}
else if (delta <= 14 * day)
{
AxisX.LabelStyle.Format = "M/d H:mm";
}
else if (delta <= 1 * year)
{
AxisX.LabelStyle.Format = "yyyy/M/d";
}
else
{
AxisX.LabelStyle.Format = "yyyy/M";
}
}
起動時のイベントについて
起動時のイベントのタイミングを確認してみると、
Form1_Load
Form1_Shown
chart1_Customize
chart1_PrePaint
... (PrePaintまたはPostPaintを繰り返し)
chart1_PostPaint
chart1_Paint
となっていました。軸のプロパティ値の計算タイミングを確認するために、軸の最小値と最大値の設定をせずに起動させたところ、Customize
イベントの時点で最小値の計算がされていました。
//chart1_Customizeにて最小値の計算がされている。
Form1_Load ← Double.NaN
Form1_Shown ← Double.NaN
chart1_Customize ← -1
chart1_PrePaint
... (PrePaintまたはPostPaintを繰り返し)
chart1_PostPaint
chart1_Paint
また軸の値を画面上での位置に変換するValueToPixelPosition
についても確認したところ、最初のPrePaint
イベント以降で有効でした。
//最初のchart1_PrePaint以降であれば計算可能。
chart1_Customize ← 0
chart1_PrePaint ← 84.0...
... (PrePaintまたはPostPaintを繰り返し)
chart1_PostPaint
chart1_Paint
autoIntervalAdjust関数内では軸のプロパティ値とValueToPixelPosition
を使用しています。そのため、起動時に目盛間隔の自動調整をする場合は、最初のPrePaint
以降で実施する必要があります。実装としては、Paintイベントが最後に1回だけ行われているので、ここで実施するようにしています。