SVG::Graph（svg-graph gem）で散布図を描く

pure Ruby で手軽に使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/32bd38e2-187c-8040-63e4-42a87cdc29b6.png)


<!--
Ruby + SVG::Graph（svg-graph gem）で散布図を描く
-->

<!--
svg-graph | RubyGems.org | コミュニティのGemホスティングサービス
-->
https://rubygems.org/gems/svg-graph

<!--
lumean/svg-graph2: SVG:::Graph is a pure Ruby library for generating charts, which are a type of graph where the values of one axis are not scalar. SVG::Graph has a verry similar API to the Perl library SVG: :Graph, and the resulting charts also look the same. This isn't surprising, because SVG::Graph started as a loose port of SVG: :Graph, although the internal code no longer resembles the Perl original at all.
-->

https://github.com/lumean/svg-graph2

<!--
リポジトリ名は svg-graph2 となっていますが、これで合ってます。
-->

# 準備

普通にインストールするだけ。

```
gem install svg-graph
```

ただし、 Ruby 3.0.0 で REXML が bundled gem になったようなので、3.0.0 以降の場合は `gem install rexml` も必要です。




# 最低限の例

SVGファイルに出力する最低限の例です。

```ruby
require "svggraph"

options = {}

g = SVG::Graph::Plot.new(options) # options は必須

points = [
  1.4, 11,
  1.9, 22,
  2.5, 33,
  4.1, 44,
  4.5, 55
]

g.add_data(
  data: points,
  title: "系列1" # 必須
)

svg = g.burn
File.write("sample_min_1.svg", svg)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/c09e3799-c769-7522-f924-d25af3d9aa04.png)


デフォルト設定では、データ点をつなぐ線とデータ点の脇の値が表示されます。あと、SVG なので日本語も普通に使えます。

<!--
点だけの表示にしたいのと、きりがよい目盛にしたい場合はこう。
-->

点だけの表示にする＋きりがよい目盛にするための設定も追加したものがこちら。


```ruby
require "svggraph"

options = {
  scale_x_integers: true,
  scale_y_integers: true,
  show_lines: false,
  show_data_values: false,
  min_x_value: 0,
  min_y_value: 0,
  scale_y_divisions: 10, # y軸目盛の間隔
}

g = SVG::Graph::Plot.new(options)

points = [
  1.4, 11,
  1.9, 22,
  2.5, 33,
  4.1, 44,
  4.5, 55
]

g.add_data(
  data: points,
  title: "系列1  " # 末尾のスペースは幅の調節用
)

svg = g.burn
File.write("sample_min_2.svg", svg)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/24110ea3-5d6d-20f1-2893-8a31c6361a28.png)

最低限としてはこんなところでしょうか。




# 環境

```
Ubuntu Linux 18.04
ruby -v #=> ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
gem:
  rexml (3.2.5)
  svg-graph (2.2.1)
```




# しくみ

- REXML で直にXMLを組み立てている




# メモ

- pure Ruby + REXML なのでインストールは楽 :thumbsup: :thumbsup: :thumbsup: :thumbsup: 
  - gnuplot などを別途インストールする必要もなし（[ruby_gnuplot](https://rubygems.org/gems/gnuplot) や [Numo::Gnuplot](https://rubygems.org/gems/numo-gnuplot) よりもさらに依存が少ない）
- SVG::Graph という名前の通り、出力フォーマットは SVG のみ
- SVG::TT::Graph という Perl の ライブラリの移植としてスタートしたとのこと
- 散布図の場合は主に `Graph.rb` と `Plot.rb` を見ればよくて、  コメント行を除いた合計は 1,400行弱、ヒアドキュメントで書かれている CSS を除外すると約 1,100行。  なので、使い方が分からないときやうまく動かないときでもサッと読めるサイズ感。

このライブラリだけでどんな図でも描ける！ という感じではなさそうですが、pure Ruby かつ小規模・少依存なので、最初の選択肢として検討するとよさそうです。これで十分用が足りるならこれでいいでしょう、っていう。


<!--
cat Graph.rb | grep -v '^ *#' | cat -n
937
cat Plot.rb | grep -v '^ *#' | cat -n
446

825
281
(+ 825 281)
1106

-->


# 自分が使いそうな機能についてメモ

設定項目についてはコメントで説明されています。まずはこれを見ましょう。

https://github.com/lumean/svg-graph2/blob/master/lib/SVG/Graph/Graph.rb
https://github.com/lumean/svg-graph2/blob/master/lib/SVG/Graph/Plot.rb

rubydoc.info では以下（v2.2.1 のもの）:

<!--
Class: SVG::Graph::Graph — Documentation for svg-graph (2.2.1)
Class: SVG::Graph::Plot — Documentation for svg-graph (2.2.1)
-->
https://www.rubydoc.info/gems/svg-graph/2.2.1/SVG/Graph/Graph
https://www.rubydoc.info/gems/svg-graph/2.2.1/SVG/Graph/Plot




## グラフのタイトル, 軸タイトル

```ruby
options = {
  graph_title: "タイトル",
  show_graph_title: true,
  x_title: "x軸タイトル",
  show_x_title: true,
  y_title: "y軸タイトル",
  show_y_title: true,
  # ...
}

g = SVG::Graph::Plot.new(options)

# ...
```



## 複数系列の描画

`Plot#add_data` を複数回呼んで追加する方式。

```ruby
g.add_data(
  data: points1,
  title: "系列1"
)

g.add_data(
  data: points2,
  title: "系列2"
)
```



## スタイルシートを一部だけ変えたい

たとえば、軸タイトルはデフォルトでは赤い文字で描画されます。これを黒に変えたいという場合。

```ruby
svg = g.burn

svg.sub!("</style>", <<~STYLE)
  .xAxisTitle { fill: #000000; }
  .yAxisTitle { fill: #000000; }
  </style>
STYLE
```

ちょっと強引ですが、一番手っ取り早いのはこれですかね……。

`SVG::Graph#inline_style_sheet` という設定項目が存在しているのですが、スタイルシートをまるごと切り替える動作になっているようです。

<!--
一部だけ変えたいという場合はちょっと使いにくいかなと思いました。
-->





## 系列ごとに点・線の表示を指定したい

### ソースを修正する

:::note info
（2022-09-24 追記）
ソースを修正せずに済む方法もありました。次の節を参照してください。
:::

散布図を描いたら回帰直線も描画したいところですが、現状だと点や線の表示・非表示は系列ごとではなくグラフ全体での設定になっています。

ただ、雑に試してみたところ、下記のように改造すれば系列ごとの設定が可能でした。雑です。

```diff
--- a/lib/SVG/Graph/Plot.rb
+++ b/lib/SVG/Graph/Plot.rb
@@ -402,17 +402,18 @@ module SVG
             })
           end
 
-          if show_lines
+          if data.fetch(:show_lines, show_lines)
             @graph.add_element( "path", {
               "d" => "M#{x_start} #{y_start} #{lpath}",
               "class" => "line#{line}"
             })
           end
 
-          if show_data_points || show_data_values || add_popups
+          _show_data_points = data.fetch(:show_data_points, show_data_points)
+          if _show_data_points || show_data_values || add_popups
             x_points.each_index { |idx|
               c = calc_coords(x_points[idx] -  x_min, y_points[idx] -  y_min)
-              if show_data_points
+              if _show_data_points
                 shape_selection_string = data[:description][idx].to_s
                 if !data[:shape][idx].nil?
                   shape_selection_string = data[:shape][idx].to_s
```

`Plot#draw_data` は 50行程度で、モンキーパッチで対応できなくもなさそう。

ためしに描いてみたもの:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/8a848990-7efb-51cd-4dff-48d504fcc297.png)


直線が引ければ曲線も引ける:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/bbc16281-738f-81c9-3475-a55cf1050112.png)



### スタイルシートで制御する

（この節は 2022-09-24 に追記しました）
ソースの修正やモンキーパッチをしなくてもスタイルシートの指定で対応できました。

- グラフ全体のオプションで `show_lines: true` と指定しておく
- 系列ごとに `display: none` で線や点を非表示にする

という方法です。

```ruby
require "svggraph"

options = {
  show_data_values: false,
  show_lines: true,

  scale_x_integers: true,
  scale_y_integers: true,
  min_x_value: 0,
  min_y_value: 0,
}

g = SVG::Graph::Plot.new(options)

points1 = [ 1.0, 11,  1.8, 25,  3.1, 32 ]
points2 = [ 1.0, 21,  1.8, 35,  3.1, 42 ]

g.add_data(data: points1, title: "series 1")
g.add_data(data: points2, title: "series 2")

svg = g.burn

svg.sub!("</style>", <<~STYLE)
  .line1 { display: none; }
  .dataPoint2 { display: none; }
</style>
STYLE

File.write("sample.svg", svg)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/054c0483-a642-8a65-ef29-51075409b1b6.png)






## 時系列データ

`Plot` クラスを継承した `TimeSeries` というクラスがあります。
以下を参照。

- https://github.com/lumean/svg-graph2/blob/master/lib/SVG/Graph/TimeSeries.rb
- https://github.com/lumean/svg-graph2/tree/master/examples/timeseries.rb
- https://github.com/lumean/svg-graph2/tree/master/examples/timeseries.svg




# まとめたもの

```ruby
require "svggraph"

options = {
  scale_x_integers: true,
  scale_y_integers: true,

  graph_title: "タイトル",
  show_graph_title: true,
  x_title: "x軸タイトル",
  show_x_title: true,
  y_title: "y軸タイトル",
  show_y_title: true,

  # x, y の範囲
  min_x_value: 0,
  max_x_value: 6,
  min_y_value: 0,
  max_y_value: 60,

  scale_y_divisions: 10, # y軸目盛の間隔
}

g = SVG::Graph::Plot.new(options)

points1 = [
  1.0, 11,
  1.8, 25,
  3.1, 32,
]

points2 = [
  1.0, 21,
  1.8, 35,
  3.1, 42,
]

g.add_data(
  data: points1,
  title: "系列1  "
)

g.add_data(
  data: points2,
  title: "系列2"
)

svg = g.burn

svg.sub!("</style>", <<~STYLE)
  .xAxisTitle { fill: #000000; }
  .yAxisTitle { fill: #000000; }
</style>
STYLE

File.write("sample_full.svg", svg)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/32bd38e2-187c-8040-63e4-42a87cdc29b6.png)




# IRuby 対応

`to_iruby` メソッドが生えているので IRuby だとこんな感じで表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/f34db509-8819-ae71-53e9-2359933b91c4.png)

参考: [Automatically show charts in jupyter · Issue #13 · lumean/svg-graph2](https://github.com/lumean/svg-graph2/issues/13)








# この記事を読んだ人は（ひょっとしたら）こちらも読んでいます

<!--
Ruby + Victor でSVGお絵描き（簡単な散布図を描いてみた） - Qiita
-->
https://qiita.com/sonota88/items/7e2ab396163c081657b1

<!--
Galaaz を触ってみた（TruffleRuby + ggplot2 で散布図を描いてみた） - Qiita
-->
https://qiita.com/sonota88/items/b586e96f7805a5695a34