2009年7月21日火曜日

Google App Engine: 簡単にグラフ・チャートを作成する

このブログ記事をはてなブックマークに追加

ブログなどでちょっとしたグラフを表示させたい場合、Google Chart APIを使っていた。しかし、手作業で入力するのはかなり面倒だ。ウェブ上にはグラフやチャートを作成するサービスなどもあるが、登録が必要だったり、手順が面倒だったりと、個人的にはあまり手軽だと思えない。数値をコピー&ペーストしてクリック一つでグラフを作成したいのだ。そこで、使いたいサービスは自分で作ってしまえということで、Google App Engine (GAE)とGoogle Chart APIを使って簡単にグラフ・チャートを作成するウェブアプリを作ってみた。



即席で作成したウェブアプリなので不備な点もあるが、取り敢えず自分で使う分にはこの程度で十分なのでGAEに登録しておいた。気が向いたら機能を拡張していくかもしれない。

以下に使い方を示す。

使い方

まず、以下のデータをテキストエリアに入れてみる。

1.2 2.2 3.5 5.6 2.8

次に、「作成」ボタンを押す。チャートの種類が「折れ線グラフX」になっていれば、入力データをもとに以下のようなグラフが描かれる。


グラフのX軸、Y軸の下限と上限、およびその目盛り分割は自動で行われる。もちろん、手動での設定もできるし、自動で設定された数値を修正することも可能だ。また、出力画像ファイルの大きさも、グラフの色(16進数6桁[RGB]で表示)も変更できる。タイトルを表示したければタイトル欄に入力すればよい。因みにタイトルを改行させるには | を入れる。さらに、下記のようなGoogle Chart APIによるURLも表示するようにした。このURLを画像URLとしてブログなどで利用すれば、上のグラフを表示させるのも簡単だ。

http://chart.apis.google.com/chart?chs=300x200&chd=t:1.2,2.2,3.5,5.6,2.8&chds=1,6&cht=lc&chco=4d89f9,c6d9fd&chxt=y&chxl=0:|1|2|3|4|5|6

現時点で利用できるグラフの種類は、「折れ線グラフX」、「折れ線グラフXY」、「スパークライン」、「積上横棒グラフ」、「積上縦棒グラフ」、「集合横棒グラフ」、「集合縦棒グラフ」、「円グラフ」、「3D円グラフ」、「散布図」の10種類だ。2カラム以上のデータが必要なグラフは、「折れ線グラフXY」と「散布図」で、ほかは1カラム以上のデータとなる。また、「転置」チェックボックスにチェックを入れると行と列を入れ替えることができ、「ラベル」チェックボックスにチェックを入れると、入力データの1カラム目がラベルとなる。ラベル内に空白を入れたい場合は、空白を+に置き換えること。

データA 0.113 データB 0.243 データC 0.543 データD 0.443

上記のデータを入力し、「集合横棒グラフ」で作成すると、以下の棒グラフが描画される。因みに横棒グラフについては、X軸とY軸が入れ替わるので注意して欲しい。


また、「円グラフ」、「3D円グラフ」で作成すると以下の出力になる。



2カラムの数値データとして以下のデータを入力する。

0.113 0.223 0.343 0.285 0.443 0.386 0.543 0.192 0.723 0.256

これを、「折れ線グラフXY」で作成すると、以下の折れ線グラフを出力する。


さらに、同じデータまま、「積上横棒グラフ」を作成。


そして、「散布図」は次のようになる。


以下にソースコードを示す。

# 簡易グラフ・チャート作成 class Chart(webapp.RequestHandler): def __init__(self): self.chart_url = "" self.chart_html = "" self.chart_form = "" self.width = 300 self.height = 200 self.has_label = False self.is_trans = False self.title = "" self.color = "4d89f9,c6d9fd" self.is_clear_x = False self.is_clear_y = False self.min_x = None self.max_x = None self.step_x = None self.min_y = None self.max_y = None self.step_y = None self.html = u""" <html> <head> <link type="text/css" rel="stylesheet" href="/stylesheets/chart.css" /> <title>簡易グラフ・チャート作成</title> </head> <body> <div class="NoBoxedItem"> <form action="/chart" method="post"> <div><select name="chart"> <option value="lc">折れ線グラフ X</option> <option value="lxy">折れ線グラフ XY</option> <option value="ls">スパークライン</option> <option value="bhs">積上横棒グラフ</option> <option value="bvs">積上縦棒グラフ</option> <option value="bhg">集合横棒グラフ</option> <option value="bvg">集合縦棒グラフ</option> <option value="p">円グラフ</option> <option value="p3">3D円グラフ</option> <option value="s">散布図</option> </select> 幅:<input type="text" name="width" size="4" value="%d"></text> 高さ:<input type="text" name="height" size="4" value="%d"></text></div> <div>タイトル: <input type="text" name="title" size="40" value="%s"></text></div> <div><textarea name="content" rows="10" cols="60">%s</textarea></div> <div>X (クリア<input type="checkbox" name="clear_x" value="clear_x" />): 下限:<input type="text" name="min_x" size="3" value="%s"></text> 上限:<input type="text" name="max_x" size="3" value="%s"> 分割:<input type="text" name="step_x" size="2" value="%s"></div> <div>Y (クリア<input type="checkbox" name="clear_y" value="clear_y" />): 下限:<input type="text" name="min_y" size="3" value="%s"></text> 上限:<input type="text" name="max_y" size="3" value="%s"> 分割:<input type="text" name="step_y" size="2" value="%s"></div> <div>色: <input type="text" name="color" size="40" value="%s"></text></div> <div><input type="submit" value="作成" /> <input type="checkbox" name="label" value="label" />ラベル <input type="checkbox" name="trans" value="trans" />転置 <span><a href="./chart" onclick="window.open('./chart'); return false;">新規</a> <a href="http://handasse.blogspot.com/2009/07/google-app-engine.html" onclick="window.open('http://handasse.blogspot.com/2009/07/google-app-engine.html'); return false;">ヘルプ</a></span></div> </form> </div> <div class="NoBoxedItem"> %s %s </div> <div class="Contents"><span style="font-size:x-small;color:#666666;">Copyright &copy; 2009 <a href="http://handasse.blogspot.com/">nox</a>. All rights reserved.</span></div> </body> </html>""" def get(self): self.response.out.write(self.html % (self.width, self.height, "", "", "", "", "", "", "", "", self.color, "", "")) def get_scale(self, min_data, max_data, axis): stepsize_log = math.log10(max_data - min_data) stepsize = 10**int(stepsize_log) if stepsize_log < 0: stepsize = Decimal(str(stepsize)) * Decimal("0.1") scalesize = float(Decimal("100") / stepsize) stepsize = float(stepsize) else: scalesize = 1 min_scale = min_data // stepsize * stepsize redun = 1 if Decimal(str(max_data)) % Decimal(str(stepsize)) == Decimal("0"): redun = 0 max_scale = float((Decimal("%.10f" % max_data) // Decimal("%.10f" % stepsize) + redun) * Decimal("%.10f" % stepsize)) if stepsize_log >= 0 and stepsize_log == int(stepsize_log): stepsize *= 0.1 if stepsize < 1: scalesize = 100.0 / stepsize if axis == "x": if self.is_clear_x or self.min_x == None or self.max_x == None or self.step_x == None: self.min_x = min_scale self.max_x = max_scale self.step_x = int(Decimal(str(max_scale - min_scale)) / Decimal(str(stepsize))) else: min_scale = self.min_x max_scale = self.max_x stepsize = float(Decimal(str(max_scale - min_scale)) / Decimal(str(self.step_x))) if stepsize < 1: scalesize = 100.0 / stepsize elif axis == "y": if self.is_clear_y or self.min_y == None or self.max_y == None or self.step_y == None: self.min_y = min_scale self.max_y = max_scale self.step_y = int(Decimal(str(max_scale - min_scale)) / Decimal(str(stepsize))) else: min_scale = self.min_y max_scale = self.max_y stepsize = float(Decimal(str(max_scale - min_scale)) / Decimal(str(self.step_y))) if stepsize < 1: scalesize = 100.0 / stepsize return min_scale, max_scale, stepsize, scalesize def post(self): try: content = cgi.escape(self.request.get('content')) cht = cgi.escape(self.request.get('chart')) self.width = int(cgi.escape(self.request.get('width'))) self.height = int(cgi.escape(self.request.get('height'))) self.title = cgi.escape(self.request.get('title')).strip() if self.title: chtt = "&chtt=%s" % self.title.replace(" ", "+") else: chtt = "" min_x = cgi.escape(self.request.get('min_x')) if min_x: self.min_x = float(min_x) max_x = cgi.escape(self.request.get('max_x')) if max_x: self.max_x = float(max_x) step_x = cgi.escape(self.request.get('step_x')) if step_x: self.step_x = int(step_x) min_y = cgi.escape(self.request.get('min_y')) if min_y: self.min_y = float(min_y) max_y = cgi.escape(self.request.get('max_y')) if max_y: self.max_y = float(max_y) step_y = cgi.escape(self.request.get('step_y')) if step_y: self.step_y = int(step_y) self.color = cgi.escape(self.request.get('color')).replace(" ", "") if self.request.get('clear_x') == 'clear_x': self.is_clear_x = True if self.request.get('clear_y') == 'clear_y': self.is_clear_y = True if self.request.get('label') == 'label': self.has_label = True else: self.has_label = False if self.request.get('trans') == 'trans': self.is_trans = True else: self.is_trans = False if content: lines = [x.strip().replace(",", " ").replace("\t", " ") for x in content.rstrip().split("\n")] sdata = [[d for d in l.split()] for l in lines] if not self.is_trans: sdata = map(list, zip(*sdata)) if self.has_label: labels = sdata[0] sdata = sdata[1:] data = [[float(d) for d in sd] for sd in sdata] if cht == "lxy" or cht == "s": min_data_x, max_data_x = data[0][0], data[0][0] d = data[0] min_data_x, max_data_x = min(min_data_x, min(d)), max(max_data_x, max(d)) min_scale_x, max_scale_x, stepsize_x, scalesize_x = self.get_scale(min_data_x, max_data_x, "x") min_data_y, max_data_y = data[1][0], data[1][0] d = data[1] min_data_y, max_data_y = min(min_data_y, min(d)), max(max_data_y, max(d)) min_scale_y, max_scale_y, stepsize_y, scalesize_y = self.get_scale(min_data_y, max_data_y, "y") min_data_x, max_data_x = "%f" % min_scale_x, "%f" % max_scale_x min_data_y, max_data_y = "%f" % min_scale_y, "%f" % max_scale_y elif cht == "bhs" or cht == "bvs": tdata = map(sum, map(list, zip(*data))) min_data_y, max_data_y = tdata[0], tdata[0] for d in tdata: min_data_y, max_data_y = min(min_data_y, d), max(max_data_y, d) min_data_y = 0 min_scale_y, max_scale_y, stepsize_y, scalesize_y = self.get_scale(min_data_y, max_data_y, "y") min_data_y, max_data_y = "%f" % min_scale_y, "%f" % max_scale_y else: min_data_y, max_data_y = data[0][0], data[0][0] for d in data: min_data_y, max_data_y = min(min_data_y, min(d)), max(max_data_y, max(d)) if cht == "bhg" or cht == "bvg" or cht == "p" or cht == "p3": min_data_y = 0 min_scale_y, max_scale_y, stepsize_y, scalesize_y = self.get_scale(min_data_y, max_data_y, "y") min_data_y, max_data_y = "%f" % min_scale_y, "%f" % max_scale_y sdata = [",".join(d) for d in sdata] chd = "t:" + "|".join(sdata) if cht == "lxy" or cht == "s": chds = "%s,%s,%s,%s" % (min_data_x.rstrip("0").rstrip("."), max_data_x.rstrip("0").rstrip("."), min_data_y.rstrip("0").rstrip("."), max_data_y.rstrip("0").rstrip(".")) else: chds = "%s,%s" % (min_data_y.rstrip("0").rstrip("."), max_data_y.rstrip("0").rstrip(".")) chco = self.color if cht == "bhg" or cht == "bhs": if self.has_label: chxt = "x,y" chxl = "0:|" + "|".join([str(n / scalesize_y) for n in range(int(min_scale_y * scalesize_y), int(max_scale_y * scalesize_y) + 1, int(stepsize_y * scalesize_y))]) chxl += "|1:|" + "|".join(labels) else: chxt = "x" chxl = "0:|" + "|".join([str(n / scalesize_y) for n in range(int(min_scale_y * scalesize_y), int(max_scale_y * scalesize_y) + 1, int(stepsize_y * scalesize_y))]) elif cht == "lxy" or cht == "s": chxt = "x,y" chxl = "0:|" + "|".join([str(n / scalesize_x) for n in range(int(min_scale_x * scalesize_x), int(max_scale_x * scalesize_x) + 1, int(stepsize_x * scalesize_x))]) chxl += "|1:|" + "|".join([str(n / scalesize_y) for n in range(int(min_scale_y * scalesize_y), int(max_scale_y * scalesize_y) + 1, int(stepsize_y * scalesize_y))]) elif cht == "p" or cht == "p3": if self.has_label: chxt = "x" chxl = "0:|" + "|".join(labels) else: chxt = "" chxl = "" else: if self.has_label: chxt = "y,x" chxl = "0:|" + "|".join([str(n / scalesize_y) for n in range(int(min_scale_y * scalesize_y), int(max_scale_y * scalesize_y) + 1, int(stepsize_y * scalesize_y))]) chxl += "|1:|" + "|".join(labels) else: chxt = "y" chxl = "0:|" + "|".join([str(n / scalesize_y) for n in range(int(min_scale_y * scalesize_y), int(max_scale_y * scalesize_y) + 1, int(stepsize_y * scalesize_y))]) self.chart_url = "http://chart.apis.google.com/chart?chs=%dx%d&chd=%s&chds=%s&cht=%s&chco=%s&chxt=%s&chxl=%s%s" % (self.width, self.height, chd, chds, cht, chco, chxt, chxl, chtt) self.chart_html = '<div><img src="%s" /></div>' % self.chart_url self.chart_form = u""" <form name="form" action="/chart" method="post"> <div><textarea name="content" rows="5" cols="60">%s</textarea></div> <div><input type="button" onclick="document.form.content.select()" value="選択" /></div> </form>""" % self.chart_url if self.min_x == None or self.max_x == None or self.step_x == None: min_x = max_x = step_x = "" else: min_x, max_x, step_x = (str(self.min_x), str(self.max_x), str(self.step_x)) if self.min_y == None or self.max_y == None or self.step_y == None: min_y = max_y = step_y = "" else: min_y, max_y, step_y = (str(self.min_y), str(self.max_y), str(self.step_y)) if self.has_label: self.html = self.html.replace(u"label\"", u"label\" checked") if self.is_trans: self.html = self.html.replace(u"trans\"", u"trans\" checked") self.response.out.write(self.html.replace("\"%s\"" % cht, "\"%s\" selected" % cht) % (self.width, self.height, self.title, content, min_x, max_x, step_x, min_y, max_y, step_y, self.color, self.chart_html, self.chart_form)) except: self.response.out.write(self.html.replace(u"\"%s\"" % cht, u"\"%s\" selected" % cht) % (self.width, self.height, self.title, content, min_x, max_x, step_x, min_y, max_y, step_y, self.color, u"エラーが発生しました。データおよび設定を確認してください。", ""))

1 コメント:

Sei-ya さんのコメント...

ありそうで無かったです。
もし、お手隙の時にレーダーチャートを
追加していただけたらとても嬉しいです。