React.js + Chart.jsでインタラクティブなグラフを書く

More than 3 years have passed since last update.


概要

例えば、以下の様にグラフと表が一緒になっているページがあるとします。使用者が同ページで新規にデータを追加した直後に、表に新データが挿入されるとともにグラフも更新されるようなインタラクティブなページを作りたく、React.jsのアプリケーション内部でChart.jsを使用してみました。React.jsの状態データ(this.state)を元にChart.jsにグラフを書かせる手法です。

Rails、CoffeeScriptJSXを使用せず書きました。

Screenshot 2015-07-14 16.25.07.png


環境


  • ruby 2.2.1

  • Rails 4.2.3

  • react-rails

  • chart-js-rails

  • bootstrap

  • font-awesome-rails



手順


グラフのキャンバスを定義。

ここで定義したキャンバスを後にメインの部品のrenderメソッドで呼び出し、キャンバスのオブジェクトを作ります。


グラフのキャンバスを定義

# React.DOMの記述を省略するための変数。

R = React.DOM

PieChartCanvas = React.createClass
render: ->
R.canvas
style: { height: 200, width: 200 }

BarChartCanvas = React.createClass
render: ->
R.canvas
style: { height: 200, width: 400 }



グラフのオブジェクトへのポインターを覚えておく。

Chart.jsによりキャンバス上に作られたグラフのオブジェクトはReact.js管理外ですので、部品全体が更新される際は、自分で破壊する必要があります。そのため、後にアクセスできるようにグラフのオブジェクトを覚えておきます。


グラフのオブジェクトへのポインターを覚えておく

@MovingRecordsApp = React.createClass

getInitialState: ->
records: @props.data
# グラフのオブジェクトを覚えておくための変数。
barChartInstance: null
pieChartInstance: null



renderメソッドでテンプレートを定義する。


テンプレートを定義する


# React.DOMの記述を省略するための変数。
R = React.DOM

# メインの部品
@MovingRecordsApp = React.createClass

getInitialState: ->
records: @props.data # 外部から供給されるデータ。
# グラフのオブジェクトを覚えておくための変数。
barChartInstance: null
pieChartInstance: null

getDefaultProps: ->
records: []

...

# グラフと額縁のテンプレート
chartsPanel: ->
R.div
className: "panel panel-blue"
R.div
className: 'panel-heading'
R.div
className: "row"
R.div
className: "col-xs-3"
R.div
className: "fa fa-home fa-5x"
R.div
className: "col-xs-9 text-right"
R.div
className: 'huge'
"Total: #{@totalVolume()}"
R.div null,
"cubic feet"
R.div
className: 'panel-body'
R.div
className: 'row text-center'
# 棒グラフ用キャンバス
R.div
className: 'col-sm-6'
React.createElement BarChartCanvas,
ref: "bar" # 後にアクセスするために使用
# 円グラフ用キャンバス
R.div
className: 'col-sm-6'
React.createElement PieChartCanvas,
ref: "chart" # 後にアクセスするために使用

render: ->
R.div
className: "app_wrapper"

# グラフと額縁
R.div null, @chartsPanel()
R.hr null

# 新規作成フォームの部品(この部品についての本題ではないので、詳細は省略しています。)
R.h2 null, "Add a new item"
React.createElement NewMovingRecordForm,
handleNewRecord: @addRecord
roomSuggestions: @props.roomSuggestions
categorySuggestions: @props.categorySuggestions
R.hr null

# 表の部品(この部品についての本題ではないので、詳細は省略しています。)
React.createElement Records,
records: @state.records,
handleDeleteRecord: @deleteRecord,
handleUpdateRecord: @updateRecord

...



React.jsのライフサイクルを利用して、グラフを書く。

React.jsにはライフサイクルメソッドがあり、部品のライフサイクルの中のある時点で処理を実行することができます。Chart.jsは、React.js内部の仮想DOMにはアクセスできないので、部品が実際にDOMに搭載された後にグラフ描写を行います。逆にReact.jsはChart.jsの処理内容を知らないので、DOM更新の度にグラフのオブジェクトを自分で破壊する必要があります。(破壊しないと、更新した後も古いグラフが残ってしまいます。)


React.jsのライフサイクルを利用して、グラフを書く

  ...

# 部品がDOMに搭載された後に、グラフを書く。
componentDidMount: ->
@drawCharts()

# 部品が更新された後に、古いグラフを破壊し新しいグラフを書く。
componentWillUnmount: ->
@state.barChartInstance.destroy()
@state.pieChartInstance.destroy()

# DOM上のキャンバスを探し、そこにグラフを描画。
drawCharts: ->
# 棒グラフ
canvas = React.findDOMNode(@refs.bar) # refを手掛かりにキャンバスを探します。
ctx = canvas.getContext("2d") # 絵を書くための場所をゲットします。
# グラフ用データを渡し、グラフのオブジェクトを作ります。
# そのオブジェクトを後ほど破壊するためにポインターを保存しておきます。
@setState.barChartInstance = new Chart(ctx).Bar(@dataForBarChart())

# 円グラフ
canvas = React.findDOMNode(@refs.chart)
ctx = canvas.getContext("2d")
@setState.pieChartInstance = new Chart(ctx).Pie(@dataForPieChart())

# 棒グラフ用データ(これはドキュメンテーションから引用した例)
# ここで実際は@state.recordsのデータを加工してデータを準備します。
dataForBarChart: ->
labels: ["January", "February", "March", "April", "May", "June", "July"]
datasets: [
{
label: "My First dataset"
fillColor: "rgba(220,220,220,0.5)"
strokeColor: "rgba(220,220,220,0.8)"
highlightFill: "rgba(220,220,220,0.75)"
highlightStroke: "rgba(220,220,220,1)"
data: [65, 59, 80, 81, 56, 55, 40]
}
{
label: "My Second dataset"
fillColor: "rgba(151,187,205,0.5)"
strokeColor: "rgba(151,187,205,0.8)"
highlightFill: "rgba(151,187,205,0.75)"
highlightStroke: "rgba(151,187,205,1)"
data: [28, 48, 40, 19, 86, 27, 90]
}
]

# 円グラフ用データ(これはドキュメンテーションから引用した例)
# ここで実際は@state.recordsのデータを加工してデータを準備します。
dataForPieChart: ->
[
{
value: 300
color:"#F7464A"
highlight: "#FF5A5E"
label: "Red"
}
{
value: 50
color: "#46BFBD"
highlight: "#5AD3D1"
label: "Green"
}
{
value: 100
color: "#FDB45C"
highlight: "#FFC870"
label: "Yellow"
}
]


(構造がわかりやすようにするため)上記の例では静的データの一例で置き換えています。

部品の更新をグラフに反映させるためには、動的に@state.recordsを加工してグラフ用データをつくる必要があります。また、グラフの種類によりデータ構造が異なります。詳しくはドキュメンテーションをご覧ください。

例えば、僕はこんな感じでグラフ用のデータを作りました。


僕はこんな感じでグラフ用のデータを作りました

  dataForPieChart: ->

source = @volumeSortedBy("room")
ary = []
colors = ["#FE2E2E", "#FE9A2E", "#FE9A2E", "#9AFE2E", "#2EFE2E", "#2EFE9A",
"#2EFEF7", "#2E9AFE", "#2E2EFE", "#9A2EFE", "#FE2EF7", "#FE2E9A"]
@shuffleArray(colors)
for item, i in source
obj =
value: item.volume
color: colors[i]
highlight: colors[i]
label: item.room
ary.push(obj)
ary

dataForBarChart: ->
source = @volumeSortedBy("category")
labels = source.map (obj) -> obj.category
data = source.map (obj) -> obj.volume
datasets = [
{
fillColor: "rgba(151,187,205,0.5)"
strokeColor: "rgba(151,187,205,0.8)"
highlightFill: "rgba(151,187,205,0.75)"
highlightStroke: "rgba(151,187,205,1)"
data: data
}
]
{ labels: labels, datasets: datasets }



全体像の一例


App.js.coffee


R = React.DOM

# Canvases for charts
PieChartCanvas = React.createClass
render: ->
R.canvas
style: { height: 200, width: 200 }

BarChartCanvas = React.createClass
render: ->
R.canvas
style: { height: 200, width: 400 }

@MovingRecordsApp = React.createClass

getInitialState: ->
records: @props.data
# Remember Chart.js instances so we can delete them later.
barChartInstance: null
pieChartInstance: null

getDefaultProps: ->
records: []

addRecord: (record) ->
records = React.addons.update(@state.records, { $unshift: [record] })
@setState records: records

deleteRecord: (record) ->
index = @state.records.indexOf record
records = React.addons.update(@state.records, { $splice: [[index, 1]] })
@replaceState records: records

updateRecord: (record, newRecord) ->
index = @state.records.indexOf record
records = React.addons.update(@state.records, { $splice: [[index, 1, newRecord]] })
@replaceState records: records

chartsPanel: ->
R.div
className: "panel panel-blue"
R.div
className: 'panel-heading'
R.div
className: "row"
R.div
className: "col-xs-3"
R.div
className: "fa fa-home fa-5x"
R.div
className: "col-xs-9 text-right"
R.div
className: 'huge'
"Total: #{@totalVolume()}"
R.div null,
"cubic feet"
R.div
className: 'panel-body'
R.div
className: 'row text-center'
R.div
className: 'col-sm-6'
React.createElement BarChartCanvas,
ref: "bar"
R.div
className: 'col-sm-6'
React.createElement PieChartCanvas,
ref: "chart"

totalVolume: ->
sum = 0
for obj in @state.records
sum += (obj.volume * obj.quantity)
sum

render: ->
R.div
className: "app_wrapper"
@chartsPanel()

R.hr null

R.h2 null, "Add a new item"
React.createElement NewMovingRecordForm,
handleNewRecord: @addRecord
roomSuggestions: @props.roomSuggestions
categorySuggestions: @props.categorySuggestions
R.hr null

React.createElement Records,
records: @state.records,
handleDeleteRecord: @deleteRecord,
handleUpdateRecord: @updateRecord

# 部品がDOMに搭載された後に、グラフを書く。
componentDidMount: ->
@drawCharts()

# 部品が更新された後に、古いグラフを破壊し新しいグラフを書く。
componentWillUnmount: ->
@state.barChartInstance.destroy()
@state.pieChartInstance.destroy()

# DOM上のキャンバスを探し、そこにグラフを描画。
drawCharts: ->
# 棒グラフ
canvas = React.findDOMNode(@refs.bar) # refを手掛かりにキャンバスを探します。
ctx = canvas.getContext("2d") # 絵を書くための場所をゲットします。
# グラフ用データを渡し、グラフのオブジェクトを作ります。
# そのオブジェクトを後ほど破壊するためにポインターを保存しておきます。
@setState.barChartInstance = new Chart(ctx).Bar(@dataForBarChart())

# 円グラフ
canvas = React.findDOMNode(@refs.chart)
ctx = canvas.getContext("2d")
@setState.pieChartInstance = new Chart(ctx).Pie(@dataForPieChart())

dataForPieChart: ->
[
{
value: 300
color:"#F7464A"
highlight: "#FF5A5E"
label: "Red"
}
{
value: 50
color: "#46BFBD"
highlight: "#5AD3D1"
label: "Green"
}
{
value: 100
color: "#FDB45C"
highlight: "#FFC870"
label: "Yellow"
}
]

dataForBarChart: ->
labels: ["January", "February", "March", "April", "May", "June", "July"]
datasets: [
{
label: "My First dataset"
fillColor: "rgba(220,220,220,0.5)"
strokeColor: "rgba(220,220,220,0.8)"
highlightFill: "rgba(220,220,220,0.75)"
highlightStroke: "rgba(220,220,220,1)"
data: [65, 59, 80, 81, 56, 55, 40]
}
{
label: "My Second dataset"
fillColor: "rgba(151,187,205,0.5)"
strokeColor: "rgba(151,187,205,0.8)"
highlightFill: "rgba(151,187,205,0.75)"
highlightStroke: "rgba(151,187,205,1)"
data: [28, 48, 40, 19, 86, 27, 90]
}
]



苦労したところ

グラフを書き換えるタイミングをライフスタイルのどこで行うのかで最初悩みました。console.logで確認しながら、いろんなパターンを試しました。グラフ更新後に古いグラフが消されていないと、古いグラフが重なってレンダリングされてしまいます。試行錯誤した結果、本件には単純にcomponentDidMountでグラフをセットして、componentWillUnmountでグラフを破壊するとうまくことが判明しました。


考察

React.jsの部品内部でChart.jsのグラフ描画することにより、ユーザーが入力した結果内容をすぐにグラフに反映させることができることがわかりました。しかも比較的簡単にできました。

現時点では、ひとつのファイルに詰め過ぎていますので、次はグラフの部品を独立した部品としてリファクタリングしようと考えています。

UPDATE: ここで学んだ結果に基づき、グラフ部品をカプセル化しました。


Github

https://github.com/mnishiguchi/InteractiveChartComponent


参考資料

以上です。