グラフの軸は意外と難しい

# はじめに・問題提起
下記のグラフをご覧ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/62c8125d-1cdb-3201-46b8-56806fd9a88b.png)
問題にしたいのは、軸の単位です。縦軸は 5 刻み、横軸は 1 刻みです。特に違和感はないと思います。
では、「5」と「1」を導くにはどうしたらいいでしょうか？というのがこの記事の議題です。グラフを描くプログラムを作る際、ぶち当たった問題です。

## いろいろな例
色々いじって問題点を考えてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/18ca4db1-a303-2a2f-facb-e39f4f39cb93.png)

# 問題点の整理
## 問題点１：わかりやすい値でないといけない、密でも疎でもいけない
２次元のグラフで見てましたが、１次元、つまり数直線を２つ組み合わせているだけなので、以降は１次元で考えます。
※ 数直線は絵を描くのがめんどくさいので、描くのを簡単に作りました。

https://github.com/yo16/number_line_maker

試行錯誤すると見えてきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/1e5d0b91-31ae-966b-402b-d67bcaece3dc.png)
上から、1、2、3、5、7、10刻みです。3と7のわかりづらさは悪意すら感じます。（7は、日にちの場合のみ有効ですね）
並べてみて気づくことは、描き方は無限にあるけど、現実的・一般的な解はこんな感じでしょうか。
- 必須要件
    - (1刻み、2刻み、5刻みのいずれか) の10のn乗倍
- 見やすい条件
    - 全体を5～10に区分けする
        - 上記では2刻みより5刻みの方が、10刻みより5刻みの方が見やすい

## 問題点２：範囲を超えたときの次の目盛を出すタイミング
別の問題として、グラフに描く点などのマーカーと、目盛の関係。

「"-10～20の5刻み"にする！」と決めたとして、20.1のデータがあった場合の話です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/0ee36837-5316-6f34-0660-6ceb6a5229f4.png)
20.1はぎりぎり乗るからそのままでいいかなぁ、だけど23.0は乗らないから、ちょっと広げたい・・・となりますね。次の目盛として、25まで作ろうか、となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/b2d2788b-2943-a879-0e40-1579ea4f351d.png)
20.1の場合は広げないけど、23.0だと広げる、その境目はいくつ？という問題です。

上で20.1なら広げなくていいかなぁと書きましたが、最後の目盛である20を超えた位置を境目とする案（ちょっとなら広げない案）は、いくつであっても、グラフ的に紛らわしいです。20までの目盛があって、その付近に点があるのだけど、厳密には20.0を超えている。私見ですが、目盛の内側に全データが入っていてほしいです。超えてるのかなーどうかなーと心配したくないというか。

一方、20.1とほんの少しはみ出るだけで、次の目盛まで出てくるのもちょっと厄介です。20.1に対して5刻みで25ならまだしも、10刻みに作ってたら次は30が出てくることになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/19281847-d32b-3a0d-c408-ca5fcce05be4.png)
なのでこの場合は、10刻みでなく、5刻みが妥当な気がします。

実は問題点１の見やすい条件の「全体を5～10に区分けする」を採用すると、10刻みのようにがばっと広がらずに、適度に広がることにつながります。別の問題でも偶然同じ条件を求めることになりました。

## 問題点３：大きな数字の問題

区分けの数が適切でも、大きな数字で範囲が小さい場合は問題がおきます。99万～100万とか。目盛り以外に文字数を考えないといけないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/53707a3f-0756-6a07-2514-70dad1ea3915.png)
上記は、1,000刻みにすると、切れ目はいいけど文字が重なって、5,000だと疎すぎる。このケースでいうと、2,000刻みにすればよさそうですが、もっと大きな数字になれば同じ問題が起きます。

これは、めんどくさそうなので放っておくことにしましたｗ 文字の幅はフォントによって違うので、文字が被るかどうかを判断するのはとても大変。（何かでやろうとしたことがあります。）
縦軸なら起こらない問題だし、数字を書かないケースもあるので、とりあず放置。
そういえば、数値を斜めとか縦に書いて被らないように回避するグラフもありますね。


## 問題点４：基準点によって端の目盛りが出ない

これは作ってるときに、発覚した問題です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/0f764b9a-8b0b-132c-e0c6-0f39fca35a98.png)
目盛りを描くための基準値を設けて、そこから、目盛分を足したり引いたりして各点の目盛を描いていたのですが、基準点と目盛幅によっては、最小値/最大値を変えないと、端の目盛が描けないです。
具体的には、0～10で2ずつの目盛りだと割り切れるので、そこだけ見るとOKそうですが、基準値を5とすると0と10の目盛が現れないです。

この問題は、データには依存せず、表現する人の要件に依存するので、どちらかというとグラフを描く人の問題と言え、適切な軸の単位を自動で考えるという意味ではちょっと違うのでは？あとゼロを基準にすることが多いので、大体大丈夫なんじゃないかと、なんとなく思う・・・。
と言い訳して、今回は放置しました。


# 間隔を自動で決める方法
では改めて要件は下記。
- 1刻み、2刻み、5刻みのいずれか　ｘ　10のn乗
- 全体を5～10に区分けする
    - 間をとって8に近い方のよいとしておく（仮）

## 実現するためのアルゴリズム全体
1. データの最小値、最大値、その幅を得る。
2. n分割してみる（n=5～10）
    1. 最小・最大を単純にn分割して、１目盛の幅を得る
    1. 1,2,5 x (10のm乗) の最も近い値を得る・・・①
    1. データの最小値を、①で丸めてfloor・・・②
    1. データの最大値を、①で丸めてceil・・・③
        - ②③ともにイコールを含む
    1. ③－②を①で分割したときの分割数を得る（nになるとは限らない）
        - → これをn分割したときの評価値とする
1. 評価値が、8に最も近いn分割・①を、目盛幅とする

## 「最も近い値を得る・・・①」の部分のアルゴリズム
1. 幅(w)に対してlog10(w)を得る
1. ceil(log10(w)) でオーダー(ord)を得る
1. 下記の５つの値のうち、log10(w)に一番近い値を得る・・・④
    1. log10(5) + ord - 1
    1. log10(1) + ord
    1. log10(2) + ord
    1. log10(5) + ord
    1. log10(1) + ord + 1
1. 10の④乗が解


## 「xで丸めてfloor、ceil」の部分のアルゴリズム
割って余りを引いたりする

# 実装
上のアルゴリズムの実装は、とりあえずgistに入れました。近い将来、使う予定があります。（コメントやらなにやらもう少し整理して）

https://gist.github.com/yo16/82465f363d7430180e48696607f55d47


# あとがき

グラフは頻繁に使っていて、手で紙に書くこともなんとなく適正な間隔はわかるし、Excelは勝手にやってくれるのですが、まじめに考えたことがないという不思議な現象でした。そしてまじめに考えるととても難しい。

自分なりにいろいろ考えたし、対応もしてるけど、これはきっとまだまだ考慮漏れのことがありそうな予感がします。真面目に使うときには、もう一度作り直したい。


## 参考

https://www.eng.niigata-u.ac.jp/~nomoto/21.html