Excelなどでグラフを書くときは、目盛りは自動で決めてくれます。
ヤフーファイナンスなどを見ていても、グラフの目盛りはそれなりに見やすく決まります。
どうやって計算しているのだろうと思って調べてみても、
あまり計算方法は示されていません。
こういう記事がありましたので参考にして作ってみました。
考え方としては
1.データの最小値min、最大値maxと その範囲(R = max - min) に対してN区分ぐらいの目盛りにしたい。ヤフーファイナンスを見ていると、3~6区分なので、5区分ぐらいとする。
2.スケール(scale ≒ 範囲R / 区分N )の値は、1, 2, 5の3通り(参考サイトの通り)とする。
3.スケールを決めたら、それに合わせて、グラフの最小値、最大値を決める。
です。
スケールの決め方も含めて、最大値最小値の決め方を以下に示します。
数値例 max = 2458, min = 2316 とともに示します。
A.範囲の桁を調べて1から10未満の数値にする。
R = max - min = 2458 - 2316 = 142
log10(R) = 2.15, int(log10(R)) = 2
keta=10^int(log10(max - min)) = 100
R / keta = 142 / 100 = 1.42
B. R / keta を基数kisu 1, 2, 5, 10 に切り上げる。さらにその上の基数を選ぶ。
基数で割って整数にしたときに1未満になる時、その基数が切り上げた後の値となる。
int(X/kisu) < 1 なら X = kisu
切上(R/keta) = 2 ==> 5
C.桁をかけて元に戻し、それを1/10にしたものをスケールとする。Bで上の基数を選んだうえで10分の1にしているので大雑把に5区分ぐらいになる。
scale = 切上(R/keta) * keta / 10 = 5 * 100 / 10 = 50
D.グラフの最大値gmax、最小値gminをスケールの倍数に決める。
gmax = scale * int(max/scale + 1) = 50 * int(2458/50 + 1) = 50 * int(49.16 + 1) = 2500
gmin = scale * int(min/scale) = 50 * int(2316 / 50) = 50 * int(46.32) = 2300
この時、データの値がもともとスケールの倍数であった時は、グラフの端にデータが来るのでそれを避けるために余裕を持たせるなら、たとえば余裕率yoyu = 0.01 を使って
gmax = scale * int(max/scale + 1 + yoyu) = 50 * int(49.16 + 1.01) = 2500
gmin = scale * int(min/scale - yoyu) = 50 * int(46.32 - 0.01) = 2300
この例では変化はありません。
以上の計算で、
max = 2458, min = 2316 から gmax = 2500, gmin = 2300, scale = 50
が求まりました。区分は4区分になりました。
この方法で30例ほどヤフーファイナンスのグラフの最大最小とスケールを計算してみたところ、すべて一致しました。
'VBA
Sub main()
Dim min as DOuble, max as Double, scale as Double
min = 2316
max = 2458
RoundMinMax(min, max, scale)
msgbox min & "," & max & "," & scale
End Sub
Function RoundMinMax(min As Double, max As Double, sca As Double)
Dim yoyu As Double
yoyu = 0.01 '最大最小がスケールの倍数とぴったりにならないよう余裕を持たせる
sca = getScaleNum(min, max)
min = sca * Int(min / sca - yoyu) 'スケールの倍数に合わせる
max = sca * Int(max / sca + 1 + yoyu) 'スケールの倍数に合わせる
End Function
Function getScaleNum(min As Double, max As Double) As Double
Dim keta As Double, res As Double
Dim kisu As Variant, i As Long
'最終的に分割数の半分程度の分割目指す
Const Ndiv = 10# '分割数
kisu = Array(1, 2, 5, 10, 20) '基数
res = max - min '間隔。
keta = 10# ^ Int(Log(res) / Log(10)) 'それの位を調べる。
res = res / keta '1以上から10未満に変換
For i = LBound(kisu) To UBound(kisu) '基数を大きくしていき
If Int(res / kisu(i)) < 1 Then '基数で割ると1未満になる時
res = kisu(i + 1) * keta / Ndiv 'その次の基数を採用(分割数の半分程度の分割目指すから)
Exit For
End If
Next i
getScaleNum = res
End Function
'結果は同じですが以下のようにした方がいいかと思います。
'分割を先にする。累進度合いを定数にして外に出す。
'分割を5にして、累進をしないというのでもよいと思うので。
Function getScaleNum(min As Double, max As Double) As Double
Dim keta As Double, res As Double
Dim kisu As Variant, i As Long
'最終的に分割数の半分程度の分割目指す
Const Ndiv = 10# '分割数
Const NRuisin = 1
kisu = Array(2, 5, 10, 20, 50) '基数
res = (max - min) / Ndiv '間隔を10分の一にする。
keta = 10# ^ Int(Log(res) / Log(10)) 'それの位を調べる。
res = res / keta '1以上から10未満に変換
For i = LBound(kisu) To UBound(kisu) '基数を大きくしていき
If Int(res / kisu(i)) < 1 Then '基数で割ると1未満になる時
res = kisu(i + NRuisin) * keta 'その次の基数を採用(分割数の半分程度の分割目指すから)
Exit For
End If
Next i
getScaleNum = res
End Function