Go言語からExcelを操作してグラフを描く　ためにライブラリを書いた

# Go言語からExcelを操作してグラフを描く
Go言語からExcelを操作してグラフを描くには、基本的には[go-ole](https://github.com/go-ole/go-ole)という便利なライブラリを用いて気合で実装する必要があります。
Excelの各種プロパティ、メソッドを操作する必要があり、素のgo-oleを用いると型チェックが無く大変面倒くさいです。

そこで、JScript風にExcelを扱うことが出来るgo-oleのラッパーを書けば[^1]だいぶ楽になるんじゃないかと思い、とりあえず作ってみたところ結構便利だったので、公開してみる事にします。

- [go-ole-msoffice](https://github.com/tanaton/go-ole-msoffice)

個人的に必要な機能と、目についた機能しか実装していないので、Excelの機能から見るとほんの一部しか扱えません。

## Goから操られたExcelがグラフを描いている様子
![aaaaaaaaaaaaaaaaaa2.gif](https://qiita-image-store.s3.amazonaws.com/0/29559/48c3337b-c390-5351-4f7f-360b8d70bb0a.gif)
「graph.exe」から操作されたExcelが「data/data.csv」を読み込んでグラフを描画して、「output.xlsx」というファイル名で保存しています。

### Goから操られたExcelが行っていること
- Excelの起動
- csvファイルのオープン
- x軸が異なる2種類(4つ)のデータを元に散布図を生成
- 罫線の設定
- 2軸の設定
- グラフタイトルの設定
- グラフレイアウトの設定
- X軸、Y1軸、Y2軸ラベルの設定
- グラフを別シートに移動
- ファイル名を変えてxlsx形式で保存
- Excelの終了

## グラフを描くソースコード
X軸とY軸情報のセットを空列で区切った以下のようなcsvデータを元にグラフを生成します。
X軸とY軸情報のセットは3つ以上あっても問題ありません。
2軸に移すデータの列やグラフタイトルなどはハードコーディングしています。

|x1|y1-1|y1-2||x2|y2-1|y2-2|
|---|---|---|---|---|---|---|
|16|118.703|118.718||0|155.126|155.182|
|47|118.696|118.71||13|155.113|155.172|
|93|118.724|118.737||31|155.085|155.145|
|128|118.717|118.73||48|155.095|155.153|

サンプルデータとソースは[ここ](https://github.com/tanaton/go-ole-msoffice/tree/master/example/excel/graph)にあります。

```go:graph.go
package main

import (
	"fmt"
	ole "github.com/go-ole/go-ole"
	"github.com/tanaton/go-ole-msoffice/excel"
	"path/filepath"
	"strings"
)

type ExcelGraph struct {
	app *excel.Application
}

type GraphItem struct {
	x     int
	count int
	rg    *excel.Range
	leg   []string
}

func main() {
	// COMの初期化
	ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE)
	// 確実に行う必要があるため
	defer ole.CoUninitialize()

	// エクセルオブジェクトの生成
	e := excel.ThisApplication()
	if e == nil {
		return
	}
	ex := ExcelGraph{app: e}
	// 生成してる感を出すためアプリケーションを表示する
	ex.app.SetVisible(true)
	// 既存のブックの読み込み
	workbooks := ex.app.GetWorkbooks()
	rp, _ := filepath.Abs("data/data.csv")
	book := workbooks.Open(rp)
	// シートの取得
	sheets := book.GetWorksheets()
	sheet := sheets.GetItem(1)
	// 空グラフの生成
	graph := sheet.ChartObjects().Add(30, 30, 500, 300)
	graph.SetName("graph.goで生成したグラフ")

	// シート内容をグラフに変換
	ex.sheetToChart(graph, sheet, []int{1, 2})
	// タイトルを設定
	ex.setGraphTitle(graph, "ぶりいくじっと")
	// グラフオブジェクトをグラフシートに移動
	chart := graph.GetChart()
	chart.Location(excel.XlLocationAsNewSheet, "グラフその1")

	// ブックを保存
	ex.app.SetDisplayAlerts(false)
	wp, _ := filepath.Abs("output.xlsx")
	book.SaveAs(wp, excel.XlWorkbookDefault)
	// ブックを閉じる
	book.Close()
	// Excelを閉じる
	ex.app.Quit()
	// メモリ解放みたいな感じ
	ex.app.Release()
}

// グラフに描画するためのデータを取得
func (ex *ExcelGraph) getGraphRange(sheet *excel.Worksheet) []GraphItem {
	x := 1
	arr := []GraphItem{}
	maxCol := sheet.GetCells().GetItem(1, sheet.GetColumns().GetCount()).GetEnd(excel.XlToLeft).GetColumn() + 1

	// 探索
	for {
		v := sheet.GetCells().GetItem(1, x).GetValue()
		if v.Value() == nil || sheet.Err != nil {
			break
		}

		// 列の探索
		leg := []string{}
		i := x + 1
		for ; i < maxCol; i++ {
			v := sheet.GetCells().GetItem(1, i).GetValue()
			if v.Value() == nil || sheet.Err != nil {
				break
			} else {
				leg = append(leg, v.ToString())
			}
		}

		item := GraphItem{
			x:     x,                       // データ開始位置
			count: ((i - 1) - (x + 1)) + 1, // データの列数
			rg:    sheet.GetRange(sheet.GetColumns().GetItem(x+1), sheet.GetColumns().GetItem(i-1)),
			leg:   leg,
		}
		arr = append(arr, item)
		x = i + 1
	}
	return arr
}

// シートからグラフを作る
func (ex *ExcelGraph) sheetToChart(g *excel.Chart, sheet *excel.Worksheet, secondary []int) {
	j := 1
	arr := ex.getGraphRange(sheet)
	if len(arr) <= 0 {
		fmt.Println("シートにグラフ化できるデータが無いみたい")
		return
	}

	priname := []string{}
	secname := []string{}
	sec := map[int]struct{}{}
	for _, it := range secondary {
		sec[it] = struct{}{}
	}
	// 一つのレンジにまとめる
	union := arr[0].rg
	for i := 1; i < len(arr); i++ {
		union = ex.app.Union(union, arr[i].rg)
	}

	chart := g.GetChart()
	// データの設定
	chart.SetSourceData(union, excel.XlColumns)
	// グラフの種類を設定
	chart.SetChartType(excel.XlXYScatterLinesNoMarkers)
	// 凡例の位置を修正
	legend := chart.GetLegend()
	legend.SetPosition(excel.XlLegendPositionBottom)
	// 要素の設定
	for _, it := range arr {
		xcell := sheet.GetCells().GetItem(2, it.x)
		for k := 1; k <= it.count; k++ {
			// 線ごとにX軸の設定
			sc := chart.SeriesCollection().Item(j)
			if _, ok := sec[j]; ok {
				// 2軸
				sc.SetAxisGroup(excel.XlSecondary)
				secname = append(secname, it.leg[k-1])
			} else {
				priname = append(priname, it.leg[k-1])
			}
			end := xcell.GetEnd(excel.XlDown)
			rg := sheet.GetRange(xcell, end)
			sc.SetXValues(rg)
			j++
		}
	}
	// X軸の名前を取得
	var at string
	v := sheet.GetCells().GetItem(1, 1).GetValue()
	if v.Value() != nil && sheet.Err == nil {
		at = v.ToString()
	} else {
		at = "横軸"
	}
	// グラフの軸についての設定
	ex.setGraphAxis(g, strings.Join(priname, " / "), at)
	// 指定した要素を第二軸へ移動
	if len(secondary) > 0 {
		ex.setGraphAxisSecondary(g, strings.Join(secname, " / "))
	}
}

// グラフの軸を設定
func (ex *ExcelGraph) setGraphAxis(g *excel.Chart, name, axistitle string) {
	chart := g.GetChart()
	cp := chart.Axes(excel.XlCategory, excel.XlPrimary)
	vp := chart.Axes(excel.XlValue, excel.XlPrimary)

	// X軸の目盛線の表示
	cp.SetHasMajorGridlines(true)
	cp.SetHasMinorGridlines(true)
	// Y軸の目盛線の表示
	vp.SetHasMinorGridlines(true)
	// 目盛線の位置を下に移動
	cp.SetTickLabelPosition(excel.XlTickLabelPositionLow)
	// X軸ラベルを表示
	cp.SetHasTitle(true)
	at := cp.GetAxisTitle()
	at.SetText(axistitle)
	// Y軸ラベルを表示
	vp.SetHasTitle(true)
	at = vp.GetAxisTitle()
	at.SetText(name)
}

// 指定した要素を第二軸に移動
func (ex *ExcelGraph) setGraphAxisSecondary(g *excel.Chart, name string) {
	chart := g.GetChart()
	// 2軸目のY軸ラベルを表示
	vs := chart.Axes(excel.XlValue, excel.XlSecondary)
	vs.SetHasTitle(true)
	at := vs.GetAxisTitle()
	at.SetText(name)
}

// タイトルを設定
func (ex *ExcelGraph) setGraphTitle(g *excel.Chart, title string) {
	chart := g.GetChart()
	chart.SetHasTitle(true)
	ct := chart.GetChartTitle()
	ct.SetText(title)
	ct.SetPosition(excel.XlChartElementPositionAutomatic)
	ct.SetIncludeInLayout(false) // タイトルをグラフと重ねる
}
```

## 感想
グラフを描くにあたり、最初はひたすらExcelプロパティとメソッドのラッパーを手書きしていましたが、途中から大体同じようなパターンに当てはまることに気が付いたので、定義ファイルからラッパーを生成するツールを作って、定義ファイルを手書きする方針に変えました。
パターン化したことでExcelと同じように操作できるOffice系のツール全体に使えるようになったので、go-ole-msofficeという名前にしています。

現在は定義ファイルを手作業で作る必要があり、それが結局のところ非常に面倒です。
MSDNをスクレイピングしようかと思いましたが現実的ではなさそうなので、将来的には.NET環境からOfficeを操作する際のプロパティやメソッドの宣言[^2]から自動生成するようにしたいです。

go-ole-msofficeはセルの操作も少しはできますが、Excelの機能を使用しないのならば[github.com/tealeg/xlsx](https://github.com/tealeg/xlsx)などの有名なライブラリを使った方がよいと思います。

## 注意事項
- Windows専用です。
- 動作させるにはExcelがインストールされている必要があります。

[^1]: VBA風ではないのは、自分がVBAを書いたことが無い＆元々JScriptで作ったツールをGoに移植することから始めたためです。
[^2]: Visual Studioの中にありそうなのでそのうち探します。