1
0

C# Chart 目盛間隔を自動調整する

Last updated at Posted at 2023-09-23

はじめに

C#のChartコントロールで描いた散布図において、目盛間隔を自動調整する方法です。横軸が日時の場合は、Chartコントロール自身が自動で目盛間隔を調整してくれます。同様のことを通常の数値目盛でも行う方法です。

chart_memori.gif

概要

大まかな流れとしては、

  1. 大体どのくらいの間隔(ピクセル)で目盛を振るかを設定する。例えば75ピクセル毎。

  2. 現在のグラフエリアの幅(ピクセル)からすると何分割して目盛を振るかを計算する。例えば幅を226として226/75=3.01。

  3. 予め設定した使用可能な分割数(例えば1,2,4,5,10,20分割)の中から分割数を選択し、決定する。例えば3.01であれば、3以上の最小値である4とする。

  4. 分割数が決まったら目盛間隔を仮設定する。例えば表示が49~251で4分割であれば(251-49)/4で50.5。

  5. 計算結果を指数表記にした上で仮数部を取得する。50.5 → 5.05E+1 → 5.05。

  6. 分割数と同様に予め設定した使用可能な目盛間隔(例えば1,2,5,10)の中から目盛間隔を選択し、決定する。仮数部が5.05であれば、5.05以上の最小値である10とする。

  7. 指数部より桁を合わせる。10E+1 → 100。

  8. 目盛間隔が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回だけ行われているので、ここで実施するようにしています。

1
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
1
0