kivyLauncher 環境でグラフ (+ kivy garden について)

これとかでもグラフ描きたかったんだよねぇ。

kivyLauncher 環境には MatplotLib は入ってない。これがあればワタシには理想だったんだけどね。

kivy 公式ドキュメントをみると、もとから kivy-garden が同梱されてんのかしら、とも思ったんだけど、違った。違った、んだけれども、「アドオン」として kivy パッケージの一味にはなる。がこのシカケがワタシにはよーわからんかった。

具体的には kivyLauncher アプリケーションフォルダの構造をこんな:

1 /sdcard/kivy/kivy-garden-graphdemo/android.txt
2 /sdcard/kivy/kivy-garden-graphdemo/libs
3 /sdcard/kivy/kivy-garden-graphdemo/libs/garden
4 /sdcard/kivy/kivy-garden-graphdemo/libs/garden/garden.graph
5 /sdcard/kivy/kivy-garden-graphdemo/libs/garden/garden.graph/__init__.py
6 /sdcard/kivy/kivy-garden-graphdemo/main.py

とすれば、(たとえば今の場合の) main.py から:

1 from kivy.garden.graph import Graph, MeshLinePlot
2 kv = '''
3 #:import Graph kivy.garden.graph.Graph
4 #...
5 '''
6 Builder.load_string(kv)

と出来る、てことなのね…。やっぱ気持ち悪いわ。標準 Python みたいに kivyLauncher 用 site-packages があればいいのに…。(libs の下に置くと kivy の一味になるってルールも、かなり受け容れがたい、です。) ⇒ kivyLauncher でなく apk として作る際に「自アプリケーションにアドオンを同梱」てのが本来のノリなので、アプリケーションに同梱、てのはわかるんだけれども、このノリは今ひとつ kivyLauncher で使う際には気持ちが悪い。

さて。http://nullege.com/ にヒットするとガッカリしませんか? なぜって「Python だぁ」といえばほんとに .py しか置かれてなくて、サンプルとして不完全だからな。このサイト、いったいどんな価値があるんだ、と毎度思う。(多分元サイトからコピーして管理してるんだと思うが、リンクだけでいいよ、まったく。) ともあれ、kivy.garden.graph のサンプルがここにある。kv ファイルが置かれてないのでこのままでは動かぬ。ばっかじゃねーの。

ちぅわけで、仕方なく自分で kv を補って…:

main.py
  1 # -*- coding: utf-8 -*-
  2 import kivy
  3 kivy.require('1.8.0')
  4 
  5 from kivy.app import App
  6 from kivy.lang import Builder
  7 from kivy.properties import ObjectProperty
  8 from kivy.uix.boxlayout import BoxLayout
  9 from kivy.clock import Clock
 10 
 11 from plyer import accelerometer
 12 
 13 ''' 
 14 This example uses Kivy Garden Graph addon to draw graphs plotting the 
 15 accelerometer values in X,Y and Z axes.
 16 The package is installed in the directory: ./libs/garden/garden.graph
 17 To read more about kivy garden, visit: http://kivy-garden.github.io/.
 18 '''
 19 from kivy.garden.graph import Graph, MeshLinePlot
 20 
 21 kv = '''
 22 #:import Graph kivy.garden.graph.Graph
 23 <AccelerometerDemo>:
 24     orientation: 'vertical'
 25 
 26     Graph:
 27         id: graph_plot
 28     ToggleButton:
 29         id: toggle_button
 30         on_release: root.do_toggle()
 31         text: 'Start Accelerometer'
 32         size_hint: 1, None
 33         height: self.texture_size[1] * 5
 34 '''
 35 Builder.load_string(kv)
 36 
 37 class AccelerometerDemo(BoxLayout):
 38     def __init__(self):
 39         super(AccelerometerDemo, self).__init__()
 40 
 41         self.sensorEnabled = False
 42         self.graph = self.ids.graph_plot
 43 
 44         # For all X, Y and Z axes
 45         self.plot = []
 46         self.plot.append(MeshLinePlot(color=[1, 0, 0, 1])) #X - Red
 47         self.plot.append(MeshLinePlot(color=[0, 1, 0, 1])) #Y - Green
 48         self.plot.append(MeshLinePlot(color=[0, 0, 1, 1])) #Z - Blue
 49 
 50         self.reset_plots()
 51 
 52         for plot in self.plot:
 53             self.graph.add_plot(plot)
 54 
 55     def reset_plots(self):
 56         for plot in self.plot:
 57             plot.points = [(0,0)]
 58 
 59         self.counter = 1
 60 
 61     def do_toggle(self):
 62         if not self.sensorEnabled:            
 63             accelerometer.enable()
 64             Clock.schedule_interval(self.get_acceleration, 1 / 20.)
 65 
 66             self.sensorEnabled = True
 67             self.ids.toggle_button.text = "Stop Accelerometer"
 68         else:
 69             accelerometer.disable()
 70             self.reset_plots()
 71             Clock.unschedule(self.get_acceleration)
 72 
 73             self.sensorEnabled = False
 74             self.ids.toggle_button.text = "Start Accelerometer"
 75 
 76     def get_acceleration(self, dt):
 77         val = accelerometer.acceleration
 78         if val[0] is None:
 79             return
 80 
 81         if (self.counter == 100):
 82             # We re-write our points list if number of values exceed 100.
 83             # ie. Move each timestamp to the left.
 84             for plot in self.plot:
 85                 del(plot.points[0])
 86                 plot.points[:] = [(i[0] - 1, i[1]) for i in plot.points[:]]
 87                 pass
 88 
 89             self.counter = 99
 90 
 91         self.plot[0].points.append((self.counter, val[0] * 5))
 92         self.plot[1].points.append((self.counter, val[1] * 5))
 93         self.plot[2].points.append((self.counter, val[2] * 5))
 94 
 95         self.counter += 1      
 96 
 97 class AccelerometerDemoApp(App):
 98     def build(self):
 99         return AccelerometerDemo()
100 
101 if __name__ == '__main__':
102     AccelerometerDemoApp().run()

libs/garden/garden.graph/__init__.pyこれ

動かしたらこんなだった:

どの程度のこと出来んのか、ドキュメントが整備されてるともいいがたい(docstring くらい)のでよくわからない。ticks を描いてる例はあったのでそれは出来るのはわかる。線を太くしたり背景色を変えたりとか簡単に出来んのかしら、とかまだ全然わかんない。けどひとまずは「気軽にグラフ」の一歩目にはなりそうだ。(ライブラリ管理は悩ましいけれど。)






さて。「グラフ描きたいよぉ」で辿り着いたのが kivy.garden であったが、kivy-gardenは色んなものが揃ってるらしい。Google Mapsの件について書いたが、garden.mapview で解決しそうだし、ひょっとすると (たぶんダメと思うが) garden.matplotlib が使えたりもするであろうか?

kivyLauncher での MatplotLib についてなんだけど、オリジナルには入ってないので、入れてみれるだろうか、とちょっとだけ検証したのね。MatplotLib って、最も依存性の小さな使い方は「backend = ‘Agg’ だけ使えればいい」(PIL だけで動く)のはずで、そこまでが pure Python だけで書かれてればひょっとして、と思ったんだけれど、_path とか色々 C で書かれててダメだった。やっぱしこれとかで kivyLauncher そのものに手を入れない限りムリね。






09:50追記:
「どの程度のこと出来んのか」について少しだけやってみた。まず Graph に渡せるパラメータは GitHub の README.md から概ねわかるが、background_color についてはソースみないとわからなかった。

それと、線の太さは…、たぶん MeshLinePlot にはこれの制御はなくて、LinePlot にはある。うーん、中途半端。てわけで:

main.py
  1 # -*- coding: utf-8 -*-
  2 import kivy
  3 kivy.require('1.8.0')
  4 
  5 from kivy.app import App
  6 from kivy.lang import Builder
  7 from kivy.properties import ObjectProperty, StringProperty
  8 from kivy.uix.boxlayout import BoxLayout
  9 from kivy.clock import Clock
 10 
 11 from plyer import accelerometer
 12 
 13 '''
 14 This example uses Kivy Garden Graph addon to draw graphs plotting the
 15 accelerometer values in X,Y and Z axes.
 16 The package is installed in the directory: ./libs/garden/garden.graph
 17 To read more about kivy garden, visit: http://kivy-garden.github.io/.
 18 '''
 19 from kivy.garden.graph import Graph, LinePlot
 20 
 21 kv = '''
 22 #:import Graph kivy.garden.graph.Graph
 23 <AccelerometerDemo>:
 24     orientation: 'vertical'
 25 
 26     Graph:
 27         id: graph_plot
 28         background_color: (0.5, 0.5, 0.5, 1)
 29         xlabel: 't'
 30         ylabel: 'accelerometer'
 31         x_grid: True
 32         y_grid: True
 33         x_grid_label: True
 34         y_grid_label: True
 35         x_ticks_minor: 5
 36         x_ticks_major: 25
 37         ymin: 0
 38         ymax: 10
 39         y_ticks_major: 1
 40 
 41     ToggleButton:
 42         id: toggle_button
 43         on_release: root.do_toggle()
 44         text: 'Start Accelerometer'
 45         size_hint: 1, None
 46         height: self.texture_size[1] * 5
 47 '''
 48 Builder.load_string(kv)
 49 
 50 class AccelerometerDemo(BoxLayout):
 51     def __init__(self):
 52         super(AccelerometerDemo, self).__init__()
 53 
 54         self.sensorEnabled = False
 55         self.graph = self.ids.graph_plot
 56 
 57         # For all X, Y and Z axes
 58         self.plot = []
 59         self.plot.append(LinePlot(color=[1, 0, 0, 1], line_width=3)) #X - Red
 60         self.plot.append(LinePlot(color=[0, 1, 0, 1], line_width=3)) #Y - Green
 61         self.plot.append(LinePlot(color=[0, 0, 1, 1], line_width=3)) #Z - Blue
 62 
 63         self.reset_plots()
 64 
 65         for plot in self.plot:
 66             self.graph.add_plot(plot)
 67 
 68     def reset_plots(self):
 69         for plot in self.plot:
 70             plot.points = [(0,0)]
 71 
 72         self.counter = 1
 73 
 74     def do_toggle(self):
 75         if not self.sensorEnabled:
 76             accelerometer.enable()
 77             Clock.schedule_interval(self.get_acceleration, 1 / 20.)
 78 
 79             self.sensorEnabled = True
 80             self.ids.toggle_button.text = "Stop Accelerometer"
 81         else:
 82             accelerometer.disable()
 83             self.reset_plots()
 84             Clock.unschedule(self.get_acceleration)
 85 
 86             self.sensorEnabled = False
 87             self.ids.toggle_button.text = "Start Accelerometer"
 88 
 89     def get_acceleration(self, dt):
 90         val = accelerometer.acceleration
 91         if val[0] is None:
 92             return
 93 
 94         if (self.counter == 100):
 95             # We re-write our points list if number of values exceed 100.
 96             # ie. Move each timestamp to the left.
 97             for plot in self.plot:
 98                 del(plot.points[0])
 99                 plot.points[:] = [(i[0] - 1, i[1]) for i in plot.points[:]]
100                 pass
101 
102             self.counter = 99
103 
104         self.plot[0].points.append((self.counter, val[0]))
105         self.plot[1].points.append((self.counter, val[1]))
106         self.plot[2].points.append((self.counter, val[2]))
107 
108         self.counter += 1
109 
110 class AccelerometerDemoApp(App):
111     def build(self):
112         return AccelerometerDemo()
113 
114 if __name__ == '__main__':
115     AccelerometerDemoApp().run()

これでこんなグラフになった:

あとみててわかったんだけど、ContourPlot なんてのもあるのね。これはあとでやってみよう…。