- 投稿日:2019-02-09T22:41:58+09:00
Jupyterで使える自作のプロットライブラリを作ってみたい。
Jupyter上で使えるプロットを自分で作って遊べないか試してみました。
ゴミでもなんでもアウトプットしていくべきとのことで、大したことのないものですが進めていきます。
plotlyとか既に色々選択肢がありますが、勉強目的と、自分でカスタマイズなどもできるように、車輪の再発明をしていきます。使うjsライブラリ
インタラクティブ且つ色々アニメーションさせたりも考えて、D3.jsをPython側から扱っていく方向で進めます。後は作業中は個別のモジュール単位などで実施し、定期的に全体のテストを流していく等すればテスト時間はそこまで気にならなくなりそうです。
(D3.js自体は以前記事にしているのでそちらも良ければ : SVGとD3.jsの入門まとめ)
jsライブラリの読み込みはどうやるの?
べたでjsやHTMLを書いたりする分にはIPython.displayパッケージ内の関数やクラスを使っていけばJupyter上で色々できますが、外部のjsファイルなどを対象としたい場合はどうすればいいのでしょう。
Jupyter上で、HTMLやdisplayメソッドを組み合わせてscriptタグでsrc指定したくらいだと、エラーで怒られるようです。
他の方のライブラリでマジックコマンドでJupyter上でD3.jsを扱えるようにするものがありましたが、今回はJupyter上で直接D3.jsを扱うのではなく、.pyファイルを経由する形でライブラリで色々やりたいところです。調べたところ、requirejsを使う形でJupyterで読み込めるとの記事があったため、そちらを利用する形にしました。
%%html <script> requirejs.config({ paths: { 'd3': ['https://d3js.org/d3.v4.min'], }, }); </script>※minの後に.jsといった拡張子は付けない形で指定します。
※v4のバージョンの個所は他のものを使う際には調整してください。後は、使いたいタイミングでrequire(['d3'], function(d3) {...といった記述をすることで、D3.jsがJupyter上で使えます。
%%html <svg id="test-svg" width="100" height="100"> </svg> <script> require(['d3'], function(d3) { d3.select("#test-svg") .append("rect") .attr("width", "100") .attr("height", "100") .attr("fill", "#ff0000"); }); </script>※htmlのマジックコマンドで記述したましたが、IPython.display.display関数とIPython.display.HTMLを組み合わせて、.py上から実行してもちゃんとJupyter上で表示されます。
テストどうしようか問題
フロントのテストに近いような印象ですが、Jupyter上で動作することを目的とするため、普通のバックエンド側の単体テストなどと比べると少し厄介です。
手動でのテストに頼る形でもいいのかもしれませんが、50とか100とかにプロットの種類がなってくると少ししんどい気配があります。
細かいアニメーションなどは目に頼る必要がありますが、それ以外のあまりテストを書く負担が重くなく、且つ書いてあると安心感が出てくる費用対効果が高そうなところはJupyterなどが絡む個所でもテストを書く形で進めます。Jupyterの起動
まずは、Jupyterを起動させないといけないため、noseのテストランナーのラッパー的なモジュールを用意しました。
そのモジュール内で、別のプロセスでJupyterを起動するようにしました。
起動コマンドの--no-browserオプションで、ブラウザを起動せずにJupyterを起動できます。後で触れますが、selenium側で別途ブラウザを立ち上げるので、ここではブラウザの起動はしない形で設定しています。
また、--portでテスト用のJupyterのポートを指定しています。通常は8888が使われ、8888が使用済みであれば8889...といった具合にポート番号が割り振られていくので、それらとテスト用のJupyterで番号が被らないように設定しています。import os import multiprocessing as mp import subprocess as sp import time ... def run_jupyter_process(): """ Start Jupyter process for testing. """ os.system( 'jupyter notebook --no-browser --port={jupyter_test_port} &'.format( jupyter_test_port=JUPYTER_TEST_PORT )) ... jupyter_process = mp.Process(target=run_jupyter_process) jupyter_process.start()また、別のプロセスにしないとJupyter起動のコマンドで処理が止まってしまう(Ctrl + CなどでJupyterを止めないと次に進まない)のでmultiprocessingモジュールを利用しています。
ただ、いつ起動が終わるのかが見えないので、起動が終わったかどうかをチェックする必要があります。以下のようなコマンドで、起動が終わって動いているJupyterの一覧が表示できるので、そのリストの中に指定したテスト用のポートのJupyterが存在するかどうかをチェックし、存在する状態になったタイミングでnoseのテストに移るようにwhile文で制御します。$ jupyter notebook list以下のようにポートやトークンなどを含めたリストが表示されます。
Currently running servers: http://localhost:8888/?token=27fc5d92e60184655a145a6ef723ff5f6349571b3cd0cb1e :: C:\Users\def is_jupyter_started(): """ Get the boolean value as to whether Jupyter for testing has been started or not. Returns ------- result : bool If it is started this function will returns True. """ out = sp.check_output( ['jupyter', 'notebook', 'list']) out = str(out) is_in = str(JUPYTER_TEST_PORT) in out if is_in: return True return False ... while not is_jupyter_started(): time.sleep(1)また、テストが終わった後に、起動したJupyterを止めないと、ポート番号がどんどんずれていったり、メモリを無駄に消費したりと好ましくありません。
アクセス時にもポート番号がずれずに固定のものだと制御が楽なので、テスト前と終わったタイミングでテスト用のポートのJupyterが起動していれば止めるようにしておきます。
以下のようなフォーマットのコマンドで任意のJupyterを止めることができます。$ jupyter notebook stop {ポート番号}def stop_jupyter(): """ Stop Jupyter of the port number used in the test. """ os.system('jupyter notebook stop {jupyter_test_port}'.format( jupyter_test_port=JUPYTER_TEST_PORT )) ... stop_jupyter() jupyter_process.terminate()テスト長くない?問題
テスト時にJupyterを起動させる都合、ちょっとテストが終わるまで長くなります。
一部のモジュールだけテストしたい、といったケースでも1分かかったりします。
仕事だと部分的なテストは3秒くらいあれば起動から終わりまで通るのでそれらと比べると少し辛いところです。
基本的にテストを流している間もぼーっとしているのは非効率なので、作業しつつ終わったら通知が来るようにしておきます。Win10環境で作業しているので、Windows 10 Toast NotificationsというPythonライブラリを使わせていただきました。
これで、テストが終わった際に画面右下に通知が表示されます。インストール :
$ pip install win10toast==0.9また、テストにはnoseライブラリを使っていますが、noseで引数に--with-xunitと--xunit-fileを指定することで、指定のパスにテストの実行結果をXMLで保存してくれるようになるようです。XML内に、テスト全体の実行件数や失敗件数、各テストの処理時間が保存されます。
XMLのパース用に、Pythonのxmlモジュールを使って値を取っていきます。import xml.etree.ElementTree as ET ... from win10toast import ToastNotifier import nose ... def run_nose_command(module_name): """ Execute the test command with the Nose library, and obtain the number of execution tests and the number of failure tests. Parameters ---------- module_name : str Name of the module to be tested. Specify in a form including a path. Omit extension specification. If an empty character is specified, all tests are targeted. Returns ------- test_num : int Number of tests executed. error_num : int Number of errors. failures_num : int Number of tests failed. """ xml_path = 'log_test.xml' nose_command = 'nosetests' if module_name != '': nose_command += ' %s' % module_name nose_command += ' --with-xunit --xunit-file={xml_path} -s -v'.format( xml_path=xml_path ) os.system(nose_command) with open(xml_path, 'r') as f: test_xml = f.read() xml_root_elem = ET.fromstring(text=test_xml) test_num = int(xml_root_elem.attrib['tests']) error_num = int(xml_root_elem.attrib['errors']) failures_num = int(xml_root_elem.attrib['failures']) return test_num, error_num, failures_num ... test_num, error_num, failures_num = run_nose_command( module_name=module_name) ... toast_msg = '----------------------------' toast_msg += '\ntest num: %s' % test_num toast_msg += '\nerror num: %s' % error_num toast_msg += '\nfailures num: %s' % failures_num toast_notifier = ToastNotifier() toast_notifier.show_toast( title='The test is completed.', msg=toast_msg, duration=5)これでテスト終了時に通知が飛んでくるようになります。音も鳴るのでよそ見していても安心。
お好みでSlackなどに調整するといいと思われます。というか、なんとなくお試しでこれ使ってみたけれど、普通にSlackでいいよね。
後は作業中は個別のモジュール単位などで実施し、定期的に全体のテストを流していく等すればテスト時間はそこまで気にならなくなりそうです。
また、テストのコマンドの引数で、Jupyterの起動をスキップするかどうかの指定を受け入れるようにも調整しました(1つの関数のみのテストなどで、Jupyterを使わない場合など)。遊びなので本格的なCI的なところまでは対応しませんが、ひとまずは個人で進めるにはこの程度で良さそうです。
(本当はPython用のlintを入れたり、Jupyterのプロセスを一度起動したら使いまわしたりした方がテストが早く終わったりで快適かと思われますが、それらは後日機会があれば少しずつ・・)seleniumでChromeのWebDriverを使う
テスト用のJupyterへのアクセスはseleniumとChromeのWebDriverを使わせていただきました。
過去、PhantomJSやらFireFoxは使ったことがありましたが、今回初のChromeです。前者二つよりも考えるべきことが少なく済んでなんだか快適です。参考にさせていただきました : Python + Selenium で Chrome の自動操作を一通り
import chromedriver_binaryとするだけでパスが通るのもシンプルでいいですね。
seleniumからJupyterのセルにスクリプトを入力できない問題
テスト時にJupyterやら起動させる点は問題がありませんでしたが、その後selenium経由でJupyterのセル内にスクリプトを入力していこうとしたところ、input周りの構造が大分複雑でうまくいきませんでした。クリックした後にseleniumのsend_keyなどだといまいちうまくいきません。どうやらtextarea関係がJupyter上では実は非表示になっているそうで・・
対策として、開く前にipynbファイルのセルの設定に直接値を設定して、それからノートのページを開くように調整しました。(テスト用であればこれだけでも十分かなと)
ただし、よく調べてみるとselenim経由でDOMを色々操作してしまえばいけそう、という情報が見つかりました。こちらの方がスマートな気がしないでもないので、後日気が向いたら調整しようと思います。
NOTE I have been successful when changing the DOM with javascript execution by making the textarea visible and then sending keys to it. (This was done by removing hidden; from the style attribute in the parent div element that it inherited from. However, I am looking for a solution which does not require altering the DOM.
python Selenium send_keys to a Jupyter notebook後は、seleniumからRUN ALLメニューなどを操作するスクリプトを書いて、アウトプットの内容を取得してテストするスクリプトを組んでひとまずはテストができそうな気配が出てきました。
アウトプットの要素のスクショがうまく取れない問題
How to take partial screenshot with Selenium WebDriver in python?
テストのためのJupyterやらseleniumなどの準備が整ったので、いざJupyter上でのD3.js経由のアウトプットの表示結果のスクショを取ってみよう・・と上記のstackoverflowの投稿を参考に進めてみたところ、なんだかスクショ領域がずれます。
なぜだろう・・と色々悩んだり調べていたところ、上記のstackoverflow内で以下のコメントを見つけました。
On MacOS (retina) there is problem that web element position in pixels dont match element position in screenshot, due to resize/ratio –
画面の比率の問題・・そういえば、タブレットで作業をしているので、150%(推奨値)に設定していたのを思い出しました。
ちょっと文字が小さい感が無きにしもですが、100%にしてみたところ正常に動作しました・・どうやら、selenium側での座標などの取得値が画面の解像度が100%ではない場合はずれてしまう模様。
てっきりJupyterのヘッダー部分などがある都合、座標がずれるのかとか考えて、非表示にする処理を追加したりしてしまいましたがそうではなかったようで・・。
環境変数的なもので、画面解像度設定のファイルを設置してもいいのですが、少し手間なので一旦100%で進めます。引数に渡したWebElement要素のスクショを取るコード(基本的にJupyter上のSVG要素を指定):
def save_target_elem_screenshot( target_elem, img_path=DEFAULT_TEST_IMG_PATH): """ Save screenshot of target element. Parameters ---------- target_elem : selenium.webdriver.remote.webelement.WebElement The WebElement for which screen shots are to be taken. img_path : str, default DEFAULT_TEST_IMG_PATH The destination path. """ driver.find_element_by_tag_name('body').send_keys( Keys.CONTROL + Keys.HOME) location_dict = target_elem.location size_dict = target_elem.size elem_x = location_dict['x'] left = location_dict['x'] top = location_dict['y'] right = location_dict['x'] + size_dict['width'] bottom = location_dict['y'] + size_dict['height'] screenshot_png = driver.get_screenshot_as_png() img = Image.open(BytesIO(screenshot_png)) img = img.crop((left, top, right, bottom)) img.save(img_path) img.close()※Jupyterの起動、seleniumの起動、Jupyterの入力のセルにテスト用のスクリプトを設定する処理、テスト用のノートを開く処理、Jupyter上のスクリプトの実行、ヘッダーと入力のセルを非表示にする処理(スクショが途切れないように)、アウトプットのSVG領域のスクショを保存する処理の流れのGIFアニメ:
一部以前使っていたJupyterの拡張機能の都合、通知の許可云々が出てきていますが、害は無いので放置します
スクショの保存結果:
ひとまずはD3.jsを経由しての四角を追加するだけのシンプルなものではありますが、このテストの流れでいけそうな気配があります。
OpenCVでの画像の比較
ヒストグラム比較
参考にさせていただきました。
スクショで取った画像のRGBのヒストグラムを比較して、ほぼ想定した通りの画像になっているのかをチェックするための処理を用意します。(将来、フォントなどの表示が少し変わっても分布の差はそこまで変わらずにテストが通る、といった状況を想定)
OpenCVのcalcHistでヒストグラムの計算、compareHistでヒストグラムの比較をします。
RGBの各チャンネルに対して実施し、それぞれの類似度の平均を取得するようにします。def compare_img_hist(img_path_1, img_path_2): """ Get the comparison result of the similarity by the histogram of the two images. This is suitable for checking whether the image is close in color. Conversely, it is not suitable for checking whether shapes are similar. Parameters ---------- img_path_1 : str The path of the first image for comparison. img_path_2 : str The path of the second image for comparison. Returns ------- similarity : float Similarity between two images. The maximum is set to 1.0, and the closer to 1.0, the higher the similarity. It is set by the mean value of the histogram of RGB channels. """ assert_img_exists(img_path=img_path_1) assert_img_exists(img_path=img_path_2) img_1 = cv2.imread(img_path_1) img_2 = cv2.imread(img_path_2) channels_list = [[0], [1], [2]] similarity_list = [] for channels in channels_list: img_1_hist = cv2.calcHist( images=[img_1], channels=channels, mask=None, histSize=[256], ranges=[0, 256] ) img_2_hist = cv2.calcHist( images=[img_2], channels=channels, mask=None, histSize=[256], ranges=[0, 256] ) similarity_unit = cv2.compareHist( H1=img_1_hist, H2=img_2_hist, method=cv2.HISTCMP_CORREL) similarity_list.append(similarity_unit) similarity = np.mean(similarity_list) return similarity試しにテストで真っ赤な画像2枚を指定して、類似度が1.0(最大)になっていることや、赤と緑の画像を比較して類似度が下がっていることを確認しました。
※注 赤と緑の画像の比較でも、青は両方とも0でそこは類似しているという判定になるので、類似度が0にはならない点に注意します。from nose.tools import assert_equal, assert_true, assert_raises, \ assert_less_equal from PIL import Image ... img = Image.new(mode='RGB', size=(50, 50), color='#ff0000') img.save(TEST_IMG_PATH_1) img.save(TEST_IMG_PATH_2) img.close() similarity = img_helper.compare_img_hist( img_path_1=TEST_IMG_PATH_1, img_path_2=TEST_IMG_PATH_2) assert_equal(similarity, 1.0) img = Image.new(mode='RGB', size=(50, 50), color='#00ff00') img.save(TEST_IMG_PATH_2) img.close() similarity = img_helper.compare_img_hist( img_path_1=TEST_IMG_PATH_1, img_path_2=TEST_IMG_PATH_2) assert_less_equal(similarity, 0.5)テストでのしきい値はそのうち様子を見て調整していくとして、これでとりあえずはエラーなんかで画像が表示されていない!とかデグレして全然違うように表示されてしまっている!といったことがチェックできます。
色ではなく形を重視して比較する方法(例えば、カラーセットの変更を加味して実装した場合のテストなど)もありますが、そちらは必要性を感じてきたら追加するようにします。実際にプロットの機能の実装を考える
ここまでが結構長かった感じですが、やっと本格的なプロットの機能を考えていきます。
最初にどういったものを作るか・・という点ですが、シンプルなプロットであればPythonで様々な選択肢があり、それを作るのでは面白みがありません。そのため、まだ他の方で作られてなさそうな、Storytelling with Data: A Data Visualization Guide for Business Professionalsの書籍に出てくるようなプロットのPythonでの実装を考えてみます。どういったプロットかというと、「極力シンプルに」「何を伝えたいのかを極力明確に」「なるべく短時間で使えたい内容を伝える」「より効果的にするためにデザインの知見を活かす」「色弱の方でも伝わる配色」といったような、スパゲッティコードならぬスパゲッティグラフを回避するためのプロットです。
プレゼンで聞いている方への説明で短時間で伝えないといけない際など、ビジネスで役立ちます。
詳細は著者の方のサイトのhow I storyboardなどもご確認ください。まずは一つのラインのみ目立たせる折れ線グラフから
ベーシックなものを作っていきます。
伝えたい内容が一つの数値だけで、他の要素はあまり重要ではないようなケースで使うようなプロットを考えます。
折れ線グラフで、目立たせるものを青色、他をグレーの配色にします。(色弱の方でも区別が付きやすく、青の部分があなたが伝えたいことということが瞬時に分かるプロット)その他、以下のような点を対応します。
- 凡例を端の方でまとめる形ではなく、折れ線グラフの右端に配置するのを想定します。
- タイトル・説明文をオプションとして設定できるようにします。
- X軸の値は日付(時系列のデータ)を想定します。
- 年の表記は毎回設定する必要がないのと、X軸の表示を回転させると可読性が下がるそうなので、回転させずに年と月日で2段の表示とします。
- Y軸のラベルをオプションで設定できるようにします。(何の値なのかのラベル)
- Y軸のラベルで、前後に文字列をオプションで設定できるようにします。(例 : $や円や%記号など)
Storytelling with data for grants managersの記事に、良くない例(Before)と修正後の良い例(After)が載っています。
モックを作る
最初からJupyterへの組み込みをしながらレイアウトなどの調整をするのはしんどいため、最初はHTML単体で書いてみます。こうすることで、色々D3.jsで試行錯誤がしやすかったり、最終的にPython側から渡さないといけないパラメーターの洗い出しなどを目的とします。
こんな感じになりました。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://d3js.org/d3.v4.js"></script> <style> #test-svg { border: 1px solid #999999; } #test-svg .font { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", YuGothic, "ヒラギノ角ゴ ProN W3", Hiragino Kaku Gothic ProN, Arial, "メイリオ", Meiryo, sans-serif; } #test-svg .title { fill: #6bb2f8; font-size: 25px; } #test-svg .description { font-size: 14px; fill: #999999; } #test-svg .legend { font-size: 14px; fill: #999999; } #test-svg .stands-out-legend { font-size: 14px; fill: #6bb2f8; font-weight: bold; } #test-svg .x-axis path, #test-svg .y-axis path, #test-svg .x-axis line, #test-svg .y-axis line { stroke: #999999; shape-rendering: crispEdges; } #test-svg .x-axis text, #test-svg .x-axis-year, #test-svg .y-axis text, #test-svg .y-axis-label { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", YuGothic, "ヒラギノ角ゴ ProN W3", Hiragino Kaku Gothic ProN, Arial, "メイリオ", Meiryo, sans-serif; fill: #999999; font-size: 14px; } #test-svg .line { fill: none; stroke: #cccccc; stroke-width: 2.5; } #test-svg .stands-out-line { fill: none; stroke: #acd5ff; stroke-width: 4.0; } </style> </head> <body> <script> const SVG_ID = "test-svg"; const SVG_WIDTH = 600; const SVG_HEIGHT = 372; const OUTER_MARGIN = 20; const X_TICKS = 5; const Y_TICKS = 5; const Y_AXIS_PREFIX = ""; const Y_AXIS_SUFFIX = ""; const PLOT_TITLE_TXT = "Time series of fruit prices."; const PLOT_DESCRIPTION_TXT = "Orange price keeps stable value in the long term."; const DATASET = [{ date: new Date(2018, 0, 1), Apple: 100, Orange: 120, Melon: 250 }, { date: new Date(2018, 3, 12), Apple: 120, Orange: 150, Melon: 220 }, { date: new Date(2018, 10, 3), Apple: 110, Orange: 100, Melon: 330 }, { date: new Date(2019, 1, 10), Apple: 130, Orange: 160, Melon: 310 }] const COLUMN_LIST = ["Apple", "Melon"]; const STANDS_OUT_COLUMN_LIST = ["Orange"]; var MERGED_COLUMN_LIST = COLUMN_LIST.concat(STANDS_OUT_COLUMN_LIST); const LEGEND_DATASET = [ {key: "Apple", value: 130}, {key: "Orange", value: 160}, {key: "Melon", value: 310} ]; const LEGEND_KEY = function(d) { return d.key; } const YEAR_DATASET = [ new Date(2018, 0, 1), new Date(2019, 0, 1) ]; const Y_AXIS_MIN = 0; const Y_AXIS_MAX = 310 * 1.1; const Y_AXIS_LABEL = "Price of each fruit"; const X_AXIS_MIN = new Date(2018, 0, 1); const X_AXIS_MAX = new Date(2019, 1, 10); var svg = d3.select("body") .append("svg") .attr("width", SVG_WIDTH) .attr("height", SVG_HEIGHT) .attr("id", SVG_ID) var plotBaseLineY = 0; if (PLOT_TITLE_TXT !== "") { var plotTitle = svg.append("text") .attr("x", OUTER_MARGIN) .attr("y", OUTER_MARGIN) .attr("dominant-baseline", "hanging") .text(PLOT_TITLE_TXT) .classed("title font", true); var plotTitleBBox = plotTitle.node().getBBox(); plotBaseLineY += plotTitleBBox.y + plotTitleBBox.height; } if (PLOT_DESCRIPTION_TXT !== "") { var plotDescription = svg.append("text") .attr("x", OUTER_MARGIN) .attr("y", plotBaseLineY + 10) .attr("dominant-baseline", "hanging") .text(PLOT_DESCRIPTION_TXT) .classed("description font", true); var plotDesciptionBBox = plotDescription.node().getBBox(); plotBaseLineY += plotDesciptionBBox.height + 10; } var legend = svg.selectAll("legend") .data(LEGEND_DATASET, LEGEND_KEY) .enter() .append("text") .text(function(d) { return d.key; }) .attr("dominant-baseline", "central"); legend.each(function(d) { var className; if (STANDS_OUT_COLUMN_LIST.indexOf(d.key) >= 0) { className = "legend stands-out-legend font"; }else { className = "legend font"; } d3.select(this) .classed(className, true); }) var yLabelMarginAdjust = 0; if (Y_AXIS_LABEL !== "") { var yAxisLabel = svg.append("text") .text(Y_AXIS_LABEL) .attr("transform", "rotate(270)") .attr("text-anchor", "end") .attr("dominant-baseline", "text-before-edge") .classed("font y-axis-label", true); yAxisLabel.attr("x", -plotBaseLineY - OUTER_MARGIN + 1) .attr("y", OUTER_MARGIN - 3); var yAxisLabelBBox = yAxisLabel.node() .getBBox(); yLabelMarginAdjust = yAxisLabelBBox.height + 2; } var yAxisScale = d3.scaleLinear() .domain([Y_AXIS_MIN, Y_AXIS_MAX]) .range([SVG_HEIGHT - OUTER_MARGIN, plotBaseLineY + OUTER_MARGIN]); var yAxis = d3.axisLeft() .scale(yAxisScale) .ticks(Y_TICKS) .tickFormat(function (d) { var tickFormat = d; if (Y_AXIS_PREFIX !== "") { tickFormat = Y_AXIS_PREFIX + tickFormat; } if (Y_AXIS_SUFFIX !== "") { tickFormat += Y_AXIS_SUFFIX; } return tickFormat; }); var yAxisGroup = svg.append("g") .classed("y-axis font", true) .call(yAxis); var yAxisBBox = yAxisGroup .node() .getBBox(); var yAxisPositionX = OUTER_MARGIN + yAxisBBox.width + yLabelMarginAdjust; yAxisGroup.attr("transform", "translate(" + yAxisPositionX + ", 0)"); var xAxisScale = d3.scaleTime() .domain([X_AXIS_MIN, X_AXIS_MAX]) .range([yAxisPositionX, SVG_WIDTH - OUTER_MARGIN]); var yearFormat = d3.timeFormat("%Y"); var year = svg.selectAll("year") .data(YEAR_DATASET) .enter() .append("text") .text(function(d) { return yearFormat(d); }) .attr("text-anchor", "middle") .attr("x", function(d) { return xAxisScale(d); }) .attr("y", SVG_HEIGHT - OUTER_MARGIN) .classed("font x-axis-year", true); var yearBBox = year.node() .getBBox() var xAxis = d3.axisBottom() .scale(xAxisScale) .ticks(X_TICKS) .tickFormat(d3.timeFormat("%m/%d")); var xAxisGroup = svg.append("g") .classed("x-axis font", true) .call(xAxis) var xAxisBBox = xAxisGroup .node() .getBBox(); xAxisPositionY = SVG_HEIGHT - OUTER_MARGIN - xAxisBBox.height - yearBBox.height; xAxisGroup.attr( "transform", "translate(0, " + xAxisPositionY + ")"); yAxisScale.range([xAxisPositionY, plotBaseLineY + OUTER_MARGIN]); yAxis.scale(yAxisScale); yAxisGroup.call(yAxis); var legendMaxWidth = 0; svg.selectAll(".legend").each(function(d) { var width = d3.select(this) .node() .getBBox()["width"]; legendMaxWidth = Math.max(legendMaxWidth, width); }); svg.selectAll(".legend") .attr("x", function(d) { return SVG_WIDTH - OUTER_MARGIN - legendMaxWidth; }) .attr("y", function(d) { return yAxisScale(d.value); }); xAxisScale.range( [yAxisPositionX, SVG_WIDTH - OUTER_MARGIN - legendMaxWidth - 10]); xAxis.scale(xAxisScale); xAxisGroup.call(xAxis); year.attr("x", function(d) { return xAxisScale(d); }); for (var i = 0; i < MERGED_COLUMN_LIST.length; i++) { var columnName = MERGED_COLUMN_LIST[i]; var line = d3.line() .x(function (d) { return xAxisScale(d.date); }) .y(function (d) { return yAxisScale(d[columnName]); }); var line_class; if (STANDS_OUT_COLUMN_LIST.indexOf(columnName) >= 0) { className = "stands-out-line"; }else { className = "line"; } svg.append("path") .datum(DATASET) .classed(className, true) .attr("d", line); } </script> </body> </html>若干、複数回scaleを調整しているところとかはもうちょっとシンプルにできそうな気配かありますが、D3.jsのコードをもっとたくさん書いていれば段々洗練されていくのでしょう・・。
作業中、結構アンカーポイントやバウンディングボックス関係の設定で、合っているのか結構不安だったため、Chrome拡張のRuler関係のツールを多用していました。
また、色弱の方に対する表示も確認しておきます(水色と灰色の組み合わせは大丈夫だよ、と書籍に書かれていたものの一応)。
Chromatic Vision Simulatorというツールを利用させていただきました。
ちゃんと目立たせている場所とそうではない場所の区別が付くようで、大丈夫そうですね。
組み込んでJupyter上で表示できるようにする
作ったモックを組み込んでいきます。テンプレートとしてのjsとCSSのファイルを用意してそれらを文字列としてPythonで読み込み、パラメーターを置換して、最後にIPython.displayモジュール内のdisplay関数でJupyter上で表示します。
jsのテンプレートでは、以下のようにPythonの文字列のformat関数やDjangoのテンプレートのPython変数に近い感覚で、{変数名}のという形で記述しました。
... const SVG_WIDTH = {svg_width}; const SVG_HEIGHT = {svg_height}; const SVG_BACKGROUND_COLOR = "{svg_background_color}"; const SVG_MARGIN_LEFT = {svg_margin_left}; const OUTER_MARGIN = {outer_margin}; ...CSS側は、{}の括弧で書いてしまうとVS Code上でエラーになり、入力補完が効かなくなって辛い感じなので、--変数名--という形式で置換対象のパラメーターを設定しました。
... fill: --axis_text_color--; font-size: --axis_font_size--px; ...テンプレートのjsとCSS読み込み処理は以下のように対応してみました。
def read_template_str(template_file_path): """ Read string of template file. Parameters ---------- template_file_path : str The path of the template file under the template directory. e.g., storytelling/simple_line_date_series_plot.css Returns ------- template_str Loaded template string. The repr function is set. Raises ------ Exception If the file can not be found. """ file_path = os.path.join( settings.ROOT_DIR, 'plot_playground', 'template', template_file_path) if not os.path.exists(file_path): err_msg = 'Template file not found : %s' % file_path raise Exception(err_msg) with open(file_path, 'r') as f: template_str = f.read() template_str = re.sub(re.compile('/\*.*?\*/', re.DOTALL) , '', template_str) template_str = repr(template_str)[1:-1] template_str = template_str.replace('\\n', '\n') return template_strここで注意が必要な点として、Pythonの通常の文字列は{}の括弧や%などの記号が意味を持ちます。(%sなどと文字列内で記述したりなど)
{}内の記述がうまく表示されない一方で、jsなどだとこの括弧の記号が多用されます。
そのため、Python上で生の文字列として扱う必要があります。
Python上で定義する文字列であれば以下のようにクォーテーションの前にrを付けることで生(raw)の文字列として扱われます。sample_str = r'{your_variable}'一方で、今回はテンプレートのファイルを読み込んで生の文字列にする必要があります。
そういった場合にはrepr関数を使います。ただ、r記号を付けた場合と若干挙動が異なり、最初と最後に余分なクォーテーションが付与されてしまうのと、改行までエスケープされます。そのため、以下の記述で最初と最後の文字を取り除き、且つ改行を普通に改行させるために置換をしています。template_str = repr(template_str)[1:-1] template_str = template_str.replace('\\n', '\n')その他、正規表現でテンプレートの上部などに設けていたコメント部分を取り除いています。
template_str = re.sub(re.compile('/\*.*?\*/', re.DOTALL) , '', template_str)置換処理として以下のような関数を用意しました。
辞書のキーにテンプレート上のパラメーター名、値に置換するPythonのパラメーターを設定して、ループで回して置換しています。def apply_css_param_to_template(css_template_str, css_param): """ Apply the parameters to the CSS template. Parameters ---------- css_template_str : str String of CSS template. css_param : dict A dictionary that stores parameter name in key and parameter in value. Parameter name corresponds to string excluding hyphens in template. Returns ------- css_template_str : str Template string after parameters are reflected. """ for key, value in css_param.items(): key = '--%s--' % key css_template_str = css_template_str.replace(key, str(value)) return css_template_str def apply_js_param_to_template(js_template_str, js_param): """ Apply the parameters to the js template. Parameters ---------- js_template_str : str String of js template. js_param : dict A dictionary that stores parameter name in key and parameter in value. If the parameter is a list or dictionary, it is converted to Json format. Returns ------- js_template_str : str Template string after parameters are reflected. """ for key, value in js_param.items(): if isinstance(value, (dict, list)): value = data_helper.convert_dict_or_list_numpy_val_to_python_val( target_obj=value) value = json.dumps(value) key = r'{' + key + r'}' value = str(value) js_template_str = js_template_str.replace(key, value) return js_template_str一部、配列や辞書などもパラメーターで渡しているのですが、Pandasなどを経由している都合、NumPyの型の数値など(np.int64など)が紛れ込んでいるとjsonモジュールでJSON形式に変換できないので、NumPyの型の数値部分を置換する処理を挟みました。
def convert_numpy_val_to_python_val(value): """ Convert NumPy type value to Python type value. Parameters ---------- value : * The value to be converted. Returns ------- value : * The converted value. """ np_int_types = ( np.int, np.int8, np.int16, np.int32, np.int64, np.uint, np.uint8, np.uint16, np.uint32, np.uint64, ) if isinstance(value, np_int_types): return int(value) np_float_types = ( np.float, np.float16, np.float32, np.float64, ) if isinstance(value, np_float_types): return float(value) return value def convert_dict_or_list_numpy_val_to_python_val(target_obj): """ Converts the value of NumPy type in dictionary or list into Python type value. Parameters ---------- target_obj : dict or list Dictionary or list to be converted. Returns ------- target_obj : dict or list Dictionary or list after conversion. Raises ------ ValueError If dictionaries and lists are specified. """ if isinstance(target_obj, dict): for key, value in target_obj.items(): if isinstance(value, (dict, list)): target_obj[key] = convert_dict_or_list_numpy_val_to_python_val( target_obj=value ) continue target_obj[key] = convert_numpy_val_to_python_val( value=value) continue return target_obj if isinstance(target_obj, list): for i, value in enumerate(target_obj): if isinstance(value, (dict, list)): target_obj[i] = convert_dict_or_list_numpy_val_to_python_val( target_obj=value ) continue target_obj[i] = convert_numpy_val_to_python_val(value=value) continue return target_obj err_msg = 'A type that is not a dictionary or list is specified: %s' \ % type(target_obj) raise ValueError(err_msg)※若干、NumPy側で探せば関数が用意されている気配がしないでもないです・・。
表示してみる
実際にJupyter上で確認してみます。すごい適当なデータフレームを用意しました。
import pandas as pd from plot_playground.storytelling import simple_line_date_series_plot df = pd.DataFrame(data=[{ 'date': '2017-11-03', 'apple': 100, 'orange': 140, }, { 'date': '2017-12-03', 'apple': 90, 'orange': 85, }, { 'date': '2018-04-03', 'apple': 120, 'orange': 170, }, { 'date': '2018-09-03', 'apple': 110, 'orange': 180, }, { 'date': '2019-02-01', 'apple': 90, 'orange': 150, }])以下の関数内で、テンプレートの読み込みやら置換やらdisplay関数の呼び出しなどをしています。
simple_line_date_series_plot.display_plot( df=df, date_column='date', normal_columns=['apple'], stands_out_columns=['orange'], title='Time series of fruit prices.', description='Orange price keeps stable value in the long term.')無事Jupyter上で表示できました!大体完成ですが、scikit-learnなどみたくメタデータ的なものを返却したり、このプロットのテストを追加したり細かいところを対応しておきます。
PyPI登録
pipでインストールできるように、PyPI登録を進めます。
他の方が色々記事を書かれているのでここでは基本的なところは触れずに、躓いたところを中心に触れておきます。1. 同じバージョンのものは削除してももうアップできない
当たり前かもしれませんが、一度テストでTestのPyPI環境(ステージング的な環境)にアップして、後日web画面からそのバージョンを消して再度アップしようとしたら弾かれました。テスト環境でも、バージョンの番号を上げるしかなさそう?な印象です。(特に困ったりはしませんが・・)
2. ビルドの前に、過去のビルドで生成されたディレクトリを削除しておいた方が良さそう
前のものが含まれているとアップのときに弾かれたり、事故の元になりそうです。
この辺りのビルドのスクリプトは大した量ではないので後で書いておきたいところ・・。3. cssやjsをパッケージ内に含んでくれなかった
最初、.pyのモジュールしかビルド内に含んでくれませんでした。
調べたところ、setup.pyなどのビルド用のモジュールと同じフォルダにMANIFEST.inというファイルを設置して、含めるファイルを指定しないといけなかった模様です。
今回は以下のようにjsとCSSを含むように指定しました。recursive-include plot_playground *.js recursive-include plot_playground *.cssさらに、MANIFEST.inに書いただけでは含んでくれず、setup.pyのsetup関数内で以下の記述をしないと含んでくれませんでした。他の記事でMANIFEST.inだけ触れられていたものを参考にしていたのでしばらく悩むことに・・
include_package_data=True,以下の記事に上記の点が書かれていました。助かりました
Pythonのパッケージングのベストプラクティスについて考える2018ここまでできたら、ステージング環境的なTestのPyPIにアップして動かしてみます。
Windows環境で作業していたので、ついでにLinuxでも念のため確認・・ということで、Azure Notebooksのクラウドのノートを利用しました。ちょっと普通のpipコマンドと比べると、テスト環境なのでURLを指定したりで少し長いですが、無事インストールできました!
jsやCSSのテンプレートファイルも、問題なく読み込めているようです。
後は本番のPyPI環境にアップするだけですので、本番にアップします。
この段階で、お馴染みのpipコマンドでインストールできるようになります。
再び、Azure Notebooks上で試してみます。
既にテスト用のものがインストールされているので一旦アンインストールしてから実施します(ノートのカーネルも再起動しつつ)。無事インストールできました。
長かったですがこれでやっと完了です!ちょっとドキュメントを書いたりは明日以降進めます。
また、まだプロットが1つだけで寂しいので、少しずつ作っていきたいところです。今回はテスト用のコードを書いたり、D3.jsとJupyterを繋いだり・・のところなども対応や検証が必要だったので、結構時間がかかりましたが、次回からは本質的なプロットの追加作業に注力できる・・はず。おまけ
※まだドキュメントなど全然書いていません。
- 投稿日:2019-02-09T19:17:50+09:00
JavascriptでJSON
はじめに
JavascriptでJSONを扱う機会が多いのでまとめてみます。mapとかreduceとかそろそろ使いこなせるようになりたいです。
前提
扱うJSONは下のような感じにしました。気分です。最近iPod卒業して以下の3つを品定め中だからです。ランキングは一部抜粋、今Apple Musicに傾きつつあるので、他のやつのランキングはスキップで。まあだいたい同じですし。
var sample = { Music_Services:[ { Service: "Apple Music", Company: "Apple", Top_Song_Ranking:{ observation_date: "2019-02-09", ranking:[ { artist: "あいみょん", song: "マリーゴールド", rank: 1 }, { artist: "ONE OK ROCK", song: "Stand Out Fit In", rank: 2 }, { artist: "あいみょん", song: "今夜このまま", rank: 3 }, { artist: "エド・シーラン", song: "Shape of You", rank: 7 }, { artist: "あいみょん", song: "愛を伝えたいだとか", rank: 9 } ] } }, { Service: "Google Play Music", Company: "Google", Top_Song_Ranking: null }, { Service: "Spotify", Company: "Spotify AB", Top_Song_Ranking: null } ] }* データ構造は適当に作ったものでApple MusicのAPI定義とかに合わせたりしてません。
参照
レコード参照
「音楽サービスのどんなデータ入ってるのかザーッと見たい」
sample["Music_Services"].forEach((item) => { console.log(item) })↓
{ Service: 'Apple Music', Company: 'Apple', Top_Song_Ranking: { observation_date: '2019-02-09', ranking: [ [Object], [Object], [Object], [Object], [Object] ] } } { Service: 'Google Play Music', Company: 'Google', Top_Song_Ranking: null } { Service: 'Spotify', Company: 'Spotify AB', Top_Song_Ranking: null }[Object]の中身表示したかったら
console.log(JSON.stringify(item))
にすれば見れますね。プロパティ参照
「一覧だと見づらいからどんな属性があるかだけ見たい」
var firstService = sample["Music_Services"][0] Object.keys(firstService).forEach((data) => { console.log(data); })↓
Service Company Top_Song_Ranking「もっと深い属性が見たい」
Object.keys(firstService["Top_Song_Ranking"]).forEach((data) => { console.log(data); }) console.log("====") var firstSong = firstService["Top_Song_Ranking"]["ranking"][0] Object.keys(firstSong).forEach((data) => { console.log(data); })↓
observation_date ranking ==== artist song rank基本的に1つ目のレコードだけサンプル的にとってきて中を覗く感じです。レコードによってプロパティに過不足ある場合とかは対象外で。
検索
単一検索
「Apple Musicの情報だけ欲しい」
const AppleMusic = sample["Music_Services"].find((item, index) => { return (item.Service === "Apple Music") }) /*こっちでもおなじ const AppleMusic = sample["Music_Services"].find((item, index) => { if(item.Service === "Apple Music") return true }) */ console.log(AppleMusic)↓
{ Service: 'Apple Music', Company: 'Apple', Top_Song_Ranking: { observation_date: '2019-02-09', ranking: [ [Object], [Object], [Object], [Object], [Object] ] } }ここからはApple Musicしか眼中に入れません。
なので、変数 "AppleMusic"を使いまわします。複数検索
「あいみょんの曲ってどんぐらいランキング入ってるんだろ」
const Aimyon_Songs = AppleMusic["Top_Song_Ranking"]["ranking"].filter((item, index) => { return (item.artist === "あいみょん") }) console.log(Aimyon_Songs)↓
[ { artist: 'あいみょん', song: 'マリーゴールド', rank: 1 }, { artist: 'あいみょん', song: '今夜このまま', rank: 3 }, { artist: 'あいみょん', song: '愛を伝えたいだとか', rank: 9 } ]*本当は他にももっと入ってますよ
「英語タイトルの曲どんくらいランキング入ってるんだろ」
const English_Titles = AppleMusic["Top_Song_Ranking"]["ranking"].filter((item, index) => { return (item.song.match(/[a-zA-Z]/)) }) console.log(English_Titles)↓
[ { artist: 'ONE OK ROCK', song: 'Stand Out Fit In', rank: 2 }, { artist: 'エド・シーラン', song: 'Shape of You', rank: 7 } ]*おまけ
filterは複数検索の時ですね。findも兼ねますが、なんとなく使い分けたいなと。
制限
「曲名だけ一覧でバーっと見たい」
const song_names = AppleMusic["Top_Song_Ranking"]["ranking"].map(x => x["song"]) console.log(song_names)↓
[ 'マリーゴールド', 'Stand Out Fit In', '今夜このまま', 'Shape of You', '愛を伝えたいだとか' ]mapって主にこういうときに使うイメージですね。他にもあるかな。
集約
「結局ランキングにどういうアーティストが何曲ずつくらい入ってるんだろ」
const summary = AppleMusic["Top_Song_Ranking"]["ranking"].reduce((accum, current)=> { //同じアーティスト名がaccumの中にあるか検索 const element = accum.find((item) => {return item.artist === current.artist}); //あったらカウントだけする if(element){ element.count ++} //なかったらaccumに追加してあげる else{ accum.push({ artist: current.artist, count: 1 }); } return accum }, []); console.log(summary)↓
[ { artist: 'あいみょん', count: 3 }, { artist: 'ONE OK ROCK', count: 1 }, { artist: 'エド・シーラン', count: 1 } ]reduceは集約するときに使う感じですね。forEachで足していくほうがわかりやすいんですけどreduceが使えたらかっこいいなと。まだまだ慣れません。
おわりに
reduceはやはり小難しい。あいみょんすごい。もう少し追記していきたいと思っているところです。
- 投稿日:2019-02-09T16:45:34+09:00
MTG WikiのWisdom GuildへのリンクをWiki内部にするユーザースクリプト
MTG Wikiの水色背景で書かれるカード説明。
そのカード名リンクはカード単体へのリンクではなくWisdom Guildへのリンクになっている。
デッキ集などのキーカード紹介では直感的にそのカードのリンクへ飛べなく、DCG(アリーナ)プレイヤーには正直Wisdomは不要なのでリンクを書き換える。// ==UserScript== // @name MtG Wiki wisdom to wiki link // @namespace http://tampermonkey.net/ // @version 0.1 // @description try to take over the world! // @author You // @match http://mtgwiki.com/wiki/* // @grant none // ==/UserScript== (function() { 'use strict'; const URL = 'http://mtgwiki.com/wiki/' document.querySelectorAll('a.external[href^="http://whisper.wisdom-guild.net/card/"]').forEach(a => { let text = a.textContent let names = text.split('/').map(name => { return name.trim().replace(' ', '_') }) a.href = URL + names[1] + '/' + names[0] //console.log(names) }) })();カード名をURLに変換しているので、分割カードなど特殊には未対応
- 投稿日:2019-02-09T16:37:58+09:00
RailsのCoffeeScriptで共通の処理を別のファイルへ切り出したい
タイトルの通りのことをやろうとしたときの、解決方法を見つけるまでに
調べたことを自分用のまとめも兼ねて書いておきます。Rails初学者向けです。
解決策は1番下にあります。CoffeeScriptとは?
前提知識として。
CoffeeScriptとはすごく簡単に言うと、JavaScriptをより書きやすくするためのものです。
Rubyっぽくかけます。Railsで採用されています。CoffeeScriptはCoffeeScriptのままでは
使えません。CoffeeScriptで書かれたものを普通のJavaScriptに変換して使用します。
そのあたりの変換はRailsが勝手にやってくれます。
なお、RailsはCoffeeScriptもJavaScriptも併用できます
ぐぐるとすぐどんなものかわかると思いますが、書きやすさの片鱗を1ミリだけ見せると以下です。javascriptalert("Hello");coffeescript# 引数をとる関数を呼び出すときの括弧いらない。 # 行末のセミコロンいらない。 alert "Hello"CoffeeScriptで書くと、他にも色々書きやすくなる点があります。
調べてみてください。以下本題です。
やりたいこと
やりたいことは単純です。
test1.coffeesayHello = -> alert "Hello"test2.coffeesayHello = -> alert "Hello"(なお、上記をJavaScriptで書くとこう)function sayHello() { alert("Hello"); }test1.coffeeとtest2.coffeeには同じ関数が定義されています。
同じことを2回書いているので、下記のように共通処理をまとめた
common.coffeeを作り、そこから呼び出せるようにしたい。common.coffeesayHello = -> alert "Hello"test1.coffee# common.coffeeを先に読み込んでいる前提 sayHello() # common.coffeeのsayHello関数を呼び出したい(できない)test2.coffee# common.coffeeを先に読み込んでいる前提 sayHello() # common.coffeeのsayHello関数を呼び出したい(できない)しかし、コメントにも書きましたが、実際にはcommon.coffeeからsayHello関数を
test1.coffeeやtest2.cofeeから呼び出すことはできません。
しかしJavaScriptならこれができます。common.jsfunction sayHello() { alert("Hello"); }test1.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるtest2.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるCoffeeScriptだとなぜできないのか
これはCoffeeScriptの仕様です。
CoffeeScriptファイルは、そのファイルで定義したものを即時・無名関数で丸ごと包むことにより、
ローカルスコープに閉じ込めるため、他のファイルからは使用できません。何言ってるかわかりませんね。あとで説明します。
JavaScriptだとなぜできるのか
その前に、なぜJavaScriptだとできるのかを抑えておきます。
これを理解するには、以下のキーワードを知る必要があります。
- スコープ(一般的な概念)
- グローバルスコープ(プログラミングの一般的な概念)
- ローカルスコープ(プログラミングの一般的な概念)
スコープとは
スコープとは「範囲」です。イメージしやすいように、ここでは「エリア」と言い換えます。
グローバルスコープとは
グローバルスコープとは、「そこに置いたものは、どこからでも呼び出せるようになる」エリアです。
関数の外側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、どこからでも上書きしたり、呼び出すことができます。test1.js(関数funcの外側がグローバルスコープのエリア)var hoge = "global" function func() { alert(hoge); // → "global"と表示される。変数hogeはグローバルなのでfunc関数内で使える。 } alert(hoge); // → "global"と表示される。変数hogeはグローバルなのでfunc関数外でも使える。test2.js// 事前にtest1.jsが読み込まれている前提 alert(hoge); // → "global"と表示される。変数hogeはグローバルなので、ここでも使える。 // test1.jsの変数hogeは書き換えることができる。 func(); // → "global"と表示される。 var hoge = "replace!"; func(); // → "replace!"と表示される。(test1.jsの変数hogeを書き換えできたことがわかる)ローカルスコープとは
ローカルスコープとは、「そこに置いたものは、その場所でしか呼び出せない」エリアです。
関数の内側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、関数の内側以外では、上書きしたり、呼び出すことはできません。test1.js(関数funcの内側がローカルスコープのエリア)function func() { var hoge = "local" alert(hoge); // → "local"と表示される。変数hogeはローカルなのでfunc関数内で使える。 } alert(hoge); // → エラーになる。変数hogeはローカルなのでfunc関数外では使えない。test2.js// 事前にtest1.jsが読み込まれている前提 alert(hoge); // → エラーになる。変数hogeはローカルなのでここでも使えない。 // test1.jsの変数hogeは書き換えることができない。 func(); // → "local"と表示される。 var hoge = "replace!"; func(); // → "local"と表示される。(test1.jsの変数hogeを書き換えできなかったことがわかる)※但し、
var hoge
のvar
を取ると、グローバルな変数とすることもできます。
ただ、var
の有る無しでローカススコープのエリアで定義しているのに、
グローバルとローカルが切り替わってしまうため、基本的にはvarは付けたほうがいい。
なお、CoffeeScriptにはvarはもともと存在せず、グローバル変数にはできません。JavaScriptだとなぜできるのか(再掲)
ではもう一度、JavaScriptだとなぜできるのか。
再度、JavaScriptのファイルを見てみます。common.jsfunction sayHello() { alert("Hello"); }test1.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるtest2.js// common.jsを先に読み込んでいる前提 sayHello(); // common.coffeeのsayHello関数を呼び出せるcommon.jsのsayHello()はグローバルスコープのエリアで定義されたものです。
よって、test1.jsからもtest2.jsからも呼び出せます。これがJavaScriptだとできる理由です。
CoffeeScriptだとなぜできないのか(再掲)
CoffeeScriptでできない理由も再度見てみましょう。
CoffeeScriptファイルは、そのファイルで定義したものを即時・無名関数で丸ごと包むことにより、
ローカルスコープに閉じ込めるため、他のファイルからは使用できません。以下のキーワードを知る必要があります。
- 即時関数(JavaScriptの機能)
- 無名関数(JavaScriptの機能)
即時関数とは
即時関数とは、定義した関数が即実行される関数です。よくわかりませんね。
普通の関数はfunctionで定義して、その後、明示的にfunctionの名前で呼び出して実行します。
普通の関数function sayHello() { alert("Hello"); } sayHello(); // → ここで初めて実行される。即時関数は下記のように書きます。
即時関数(function sayHello() { // → ここで定義された時点で即実行される。 alert("Hello"); }());何かの初期化処理など1回だけ実行されればいい時など、その関数を再利用する
必要がない時に使います。これが即時関数です。無名関数とは
一方、無名関数とは名前の無い関数です。よくわかりませんね。
普通の関数はfunctionのあとに関数名を書きますが、無名関数はその関数名を省略した関数です。
変数に関数を入れる例で書くと以下のようになります。
(JavaScript初学者の方には、え?って思うかもしれませんが、変数は値だけでなく関数も入れられます。)普通の関数var func = function sayHello() { alert("Hello") }; func()無名関数var func = function () { alert("Hello") }; // → sayHelloという関数名を省略できます。 func()無名関数は、関数名が必要ないときに使います。
即時関数と無名関数は組み合わせることができます。
ここではこれを即時・無名関数と呼んでおきます。即時・無名関数(function () { alert("Hello"); }());CoffeeScriptからJavaScriptへの変換
CoffeeScriptからJavaScriptへの変換処理は、
変換するときに、中身を丸ごと即時・無名関数として包みます。変換前CoffeeScriptsayHello = -> alert "Hello"変換後JavaScript(function () { sayHello = function() { return alert("Hello"); }; }());関数の内側に自分が書いたコードが包まれるので、変換後は全ての変数や関数は
ローカルスコープに閉じ込められるということです。なぜこんなことをするのか。
それは、前述のローカルスコープのところで説明した効果を得たいためです。ローカルスコープとは、「そこに置いたものは、その場所でしか呼び出せない」エリアです。
関数の内側で定義された変数や関数が、このエリアに所属することになります。
このエリアで定義された変数や関数は、関数の内側以外では、上書きしたり、呼び出すことはできません。もしたくさんのCoffeeScriptファイルを取り扱うようになった時、あるファイルでせっかく
定義した関数や変数を、別のファイルの中で同じ名前の関数や変数を使って全く違う定義を
誤ってしてしまうかもしれません。それを防ぐ効果があります。この効果のために、共通処理を切り出せなくなっています。
解決策
いくつか方法があるようです。
方法1
JavaScriptにはwindowオブジェクトという特殊なオブジェクトがあります。
先程から良く使っているalert関数も実はwindowオブジェクトが持つ関数の一つです。
つまりこう書けます。このwindowは省略できるため、alertだけで普段使えています。window.alert("Hello");このwindowオブジェクトはグローバルスコープに属しています。
グローバルスコープに属しているものはどこからでも上書き・呼び出しができます。
上書きしてしまうとalertもなにも使えなくなってしまうので、このオブジェクトに
関数を追加します。common.coffeewindow.sayHello = -> alert "Hello"test1.coffeesayHello()test2.coffeesayHello()方法2
@を使います。@はCoffeeScriptに用意されたJavaScriptのthisの別名です。
JavaScriptのthisは、グローバルスコープのエリアでは、windowオブジェクトを指します。よって方法1の下記は・・・
common.coffeewindow.sayHello = -> alert "Hello"下記のように書けます。
common.coffee@sayHello = -> alert "Hello"test1.coffeesayHello()test2.coffeesayHello()終わり
以上で終わりです。
新しいバージョンのJavaScriptだとちゃんと関数の外に定義した変数や関数も
グローバルにならないように宣言する方法があるみたいです。
ただ、どこまで今存在するWebブラウザが、そのバージョンに対応しているかは
調べられていません。「ECMAScript 6」とかでぐぐると出てきます。なにかこう・・・JavaScriptって、昔から、こう、、、アレなんですよね。。。
- 投稿日:2019-02-09T15:25:21+09:00
RainyDay.jsを使ったので使用方法メモ
基本的にjavascriptほぼわからないしGithubの見方もわからない、英語もほぼ読めない初心者が、今後自分がもう一度やるときに役立つようにという目的でまとめているため、非常に初歩的なことから書いています。
やりたいこと
サイトに雨の降るエフェクトを付けたかった
結論
プラグインRainyDay.jsを入れる。
コードは下記。html<body id="body" onload="run();"> <img id="background" alt="" src="" />css#main{ z-index:1000; } img#background{ position: fixed; width: 100%; height: 100vh; top: 0; } canvas{ width: 100%; height: auto; z-index: 1; }javascriptfunction run() { var image = document.getElementById('background'); image.onload = function () { var engine = new RainyDay({ image: this, gravityAngle:Math.PI/10 }); engine.rain([ [1, 2, 8000] ]); engine.rain([ [3, 3, 0.88], [5, 5, 0.9], [6, 2, 1] ], 100); image.crossOrigin = 'anonymous'; image.src = '/rain.jpg'; }試行錯誤
サイトを探す
雨のようなエフェクトを自分のサイトに使いたいと思い、使えるプラグインを探す。
ググったところrainyday.jsが目的と合う。
プラグインの名前で検索したところ、下記の通りのサイトが引っかかる。rainyday.jsを使ってみる | cly7796.net
窓に雨を降らせる事ができるJSライブラリ「rainyday.js」! | Design Magazine
ブラウザに雨を降らすことができるrainyday.jsを使ってみたよ! | ザ・サイベース
[JS]リアルさが想像以上!ブラウザのウインドウを窓に喩えて、雨を楽しむ癒やし系スクリプト -rainyday.js | コリスしかし、上記のサイトがリンクを貼っているGithubは既に404。
代替サイトはないかと更に検索してたら、普通にGithubじゃないサイトが引っかかる。RainyDay.js : rendering raindrops with javascript
組み込む
これでファイルがDLできる(昔はわからなくて迷った)。
zipのrainydays.js-master/docs/js/rainyday.min.jsをサイトの
</body>
の前(自分のjsがあればそれよりは上)に組み込む。その上で、窓に雨を降らせる事ができるJSライブラリ「rainyday.js」! | Design Magazineを参考にして、
html<body onload="run();"> <img id="background" src="img/rain.jpg" alt="background">javascriptfunction run() { var image = document.getElementById('background'); image.onload = function() { var engine = new RainyDay({ image: this }); engine.rain([ [1, 2, 8000] ]); engine.rain([ [3, 3, 0.88], [5, 5, 0.9], [6, 2, 1] ], 100); }; image.crossOrigin = 'anonymous'; image.src = '/rain.jpg'; }とする。やりたかったこととは違ったけれど、雨の降る画像が表示された。
ちなみに、<img id="background" alt="" src="" />
のsrcが空で大丈夫なの?と思ったけれども上記javascriptのimage.src = '/rain.jpg';
で指定したものが当然表示された。やりたかったことがうまくいかない
現状
文字の後ろに雨が降るんじゃなく、どうやっても雨が前に出る。そのため、最初は画面の全体に透過pngをz-index:9999ぐらいでおいて、それに雨を降らせようかと思っていた(それだと下の要素がクリック出来ないのを忘れてた)。
javascriptfunction run() { var image = document.getElementById('background'); image.onload = function() { var engine = new RainyDay({ image: this }); engine.rain([ [1, 2, 8000] ]); engine.rain([ [3, 3, 0.88], [5, 5, 0.9], [6, 2, 1] ], 100); }; image.crossOrigin = 'anonymous'; image.src = '/rain.jpg'; //ここが画像のアドレス }の
image.src = '/rain.jpg';
をimage.src = '/rain.png';
に変更。
しかし、どういったものに変更しても画像が表示されない。
これはもしかしてpng画像不可?と判断し、諦めて画像の上に文字をいれることにする。下にある文字列の入っている
div#main
にCSSを適用。css#main{ z-index: 100; /* 99だと雨のほうが上に表示された */ }それからブラウザに雨を降らすことができるrainyday.jsを使ってみたよ! | ザ・サイベースを見ながら
cssimg#background{ position: fixed; width: 100%; height: 100%; top: 0; } canvas{ width: 100%; height: auto; z-index: 1; }これで文字列が入っていレイヤーが上になり、なおかつ背景画像が後ろ全体に広がる。
これの表示になる。ちなみに公式サイトのデモ6がちょうどやりたかったものだったのだけれど、何故かうまくいかなかった。
サイズを変更したら雨が潰れる
ところで、これを使いたいサイトは長い文章などをおいているサイトとなる。
縦に長いページで確認したところ、雨の降る頻度が非常に少ない。
開発者ツールで確認したところ、
<canvas style="position: absolute; top: 0px; left: 0px; z-index: 99;" width="1338" height="11285"></canvas>
という表示があった。つまり、縦全体に対して、縦幅が狭いときとかわらない頻度で雨をふらせているため、結果的にあまり雨が降らないように感じられるということ?っぽい。なので、
cssimg#background{ position: fixed; width: 100%; height: 100vh; top: 0; } canvas{ width: 100%; height: 100vh; z-index: 1; }に変更した。
結果、雨粒が縦につぶれて横に広がるようになった。
開発者ツールで確認したところ、
<canvas style="position: absolute; top: 0px; left: 0px; z-index: 99;" width="1338" height="11285"></canvas>
から変わっていない。
たぶんこのheight="11285"
を変更するにはrainday.min.jsを読めなきゃだめなんだろうなということまでは理解し、読めなかったので諦める。最終的に、縦幅の短いページのみで使用することとした。
ちなみに上記の表記にしても背景画像/rain.jpgもすごく伸びたり縮んだりする。微調整
下記、いじってみてなんとなくわかったもの。
何度も書くけれどもjavascriptがわからない人がやっているので、多分間違いも多い。javascript//bodyタグにつける onload=""と同じ名称にする function run() { //bodyタグ直下の<img id="background" alt="" src="" />のIDと同じものにする var image = document.getElementById('background'); image.onload = function() { var engine = new RainyDay({ //このあとメモする image: this }); //雨の降る頻度。雨粒の大きさが[]内部、それが100ミリ秒の間にこれがどこかに降るということ engine.rain([ [1, 2, 8000] ]); engine.rain([ [3, 3, 0.88], [5, 5, 0.9], [6, 2, 1] ], 100); }; image.crossOrigin = 'anonymous'; //画像アドレス image.src = '/rain.jpg'; }RainyDay.js : rendering raindrops with javascriptのオプションその他をgoogle翻訳につっこんで見てみたやつ。()内部は私が勝手につけた説明。
option
オプション デフォルト 説明 image none 画像 parentElement Optimal 効果を生成する親要素のID(上記のだとdocument.getElementById('background'); で指定してるから未記入でOK?) sound none 音のパス blur 10 ぼかし強度を画像に適用(背景画像のボケ具合) crop none 画像の一部を切り取りたい場合は、オブジェクトを切り取ります。 enableCollisions true 雨滴の衝突を有効にする(よくわからず。falseにしても雨粒はぶつかってくる) enableSizeChange true 画面サイズが変わるとキャンバスが更新される(trueにしててもよくわからないが更新されないことが何度かあった) fps 24 キャンバス更新のfpsのレンダリング gravityAngle Math.PI / 2 重力角(ぶつかった雨粒が垂れる) gravityAngleVariance 0 風の効果をシミュレートするために重力に分散を追加します(100ぐらいにすると暴風雨っぽく雨粒が左右に飛んでく) gravityThreshold 3 重力の強さ(よくわからず) methodsはrainだけなんとなく指定できれば大丈夫じゃないかと。
雨だれが落ちるほうが面白かったので、私はgravityAngleのみ指定して出来上がり。デメリット
- 重い
- (設置場所のせいだろうけれども)画像が読み込まれるのに時間がかかる
- 縦が長いと雨粒があまり落ちてこない(javascript書けたら変わると思う)
- 投稿日:2019-02-09T14:51:03+09:00
全俺が泣いた!感動のconsole.xxx
JavaScriptにはconsole.log()以外にも便利なlog出力があります。
最近知った便利なconsole.xxxを紹介します。※ この投稿のタイトルは最近話題になっていたコピーメカ を使いました。他意しかないです。
console.log()
誰もが叩くconsole.log()。devtoolsのコンソールに引数の出力結果が表示されます。
console.log() console.error(), console.warn(), console.info()
表示はconsole.logと同じ。でも文字色、背景色が代わり、目立たせる事ができます。
(infoがlogと変わらないのはなぜだろう?)
console.info()
console.warn()
console.error() console.table()
配列、オブジェクトのデータを表形式で表示します。
console.logで、いちいち展開するより超見やすいです。
最近無駄にconsole.table使ってます。
console.log() console.table() console.dir()
ディレクトリ形式で出力結果のメンバを表示できます。arrayやobjectなどの場合はconsole.logとあまり変わらないですが、DOMツリーなどを表示するときなど表示が異なります。
console.log() console.dir() console.time(), console.timeLog()
処理時間を計測するのに便利です。
console.time("タイマー名")でタイマーを設定して、
console.timeLog("タイマー名")でそのタイマーを設定しからの経過時間を出力できます。let timeDemo = () => { // タイマー"demo"のセット console.time("demo") setInterval(()=>{ // 1000msごとに今のタイマー"demo"の経過時間を表示 console.timeLog("demo") }, 1000)}実行結果
console.time(), console.timeLog() console.trace()
console.trace()が呼び出されるまでの経路を出力することができます。
この関数はどこでどのように呼び出されるのか調べる時など便利です。function trace() { console.trace() } function c() { trace() } function b() { c() } function a() { b() }実行結果
console.trace() 参考
- MDN
https://developer.mozilla.org/ja/docs/Web/API/console/timeLog- JavaScript コードレシピ集
https://gihyo.jp/book/2019/978-4-297-10368-2- Qiitaやブログのタイトルは自分で考える必要なんてなかったんだ
https://qiita.com/YumaInaura/items/0d8427bfe2d7be476d0d
- 投稿日:2019-02-09T14:51:03+09:00
便利なconsole.xxx
JavaScriptにはconsole.log()以外にも便利なlog出力があります。
最近知ったconsole.xxxを紹介します。console.log()
誰もが叩くconsole.log()。devtoolsのコンソールに引数の出力結果が表示されます。
console.log() console.error(), console.warn(), console.info()
表示はconsole.logと同じ。でも文字色、背景色が代わり、目立たせる事ができます。
(infoがlogと変わらないのはなぜだろう?)
console.info()
console.warn()
console.error() console.table()
配列、オブジェクトのデータを表形式で表示します。
console.logで、いちいち展開するより超見やすいです。
最近無駄にconsole.table使ってます。
console.log() console.table() console.dir()
ディレクトリ形式で出力結果のメンバを表示できます。arrayやobjectなどの場合はconsole.logとあまり変わらないですが、DOMツリーなどを表示するときなど表示が異なります。
console.log() console.dir() console.time(), console.timeLog()
処理時間を計測するのに便利です。
console.time("タイマー名")でタイマーを設定して、
console.timeLog("タイマー名")でそのタイマーを設定しからの経過時間を出力できます。let timeDemo = () => { // タイマー"demo"のセット console.time("demo") setInterval(()=>{ // 1000msごとに今のタイマー"demo"の経過時間を表示 console.timeLog("demo") }, 1000)}実行結果
console.time(), console.timeLog() console.trace()
console.trace()が呼び出されるまでの経路を出力することができます。
この関数はどこでどのように呼び出されるのか調べる時など便利です。function trace() { console.trace() } function c() { trace() } function b() { c() } function a() { b() }実行結果
console.trace() 参考
- MDN
https://developer.mozilla.org/ja/docs/Web/API/console/timeLog- JavaScript コードレシピ集
https://gihyo.jp/book/2019/978-4-297-10368-2- Qiitaやブログのタイトルは自分で考える必要なんてなかったんだ
https://qiita.com/YumaInaura/items/0d8427bfe2d7be476d0d
- 投稿日:2019-02-09T12:58:41+09:00
【Vue】eval電卓
概要
Vue.jsの入門として、何か作ってみたいという人向けです。
まず、通常のjsでeval電卓を作成し、Vue.jsを使って抽象化していきます。実装
以下のような電卓を、最小構成で実装する。
index.html
のonClick
イベントにより、calc
関数が呼ばれ、
クリックされたボタンに応じた処理をします。通常のjsの場合
button
タグの羅列が冗長であることが確認できる。とりあえず、動かしたい人は、cloneして下さい。
clonegit clone https://github.com/Naoto92X82V99/calc.gitindex.html<html> <head> <meta charset ="utf-8"> <script src="script.js"></script> </head> <body> <table id="calcTable"> <tr> <td colspan="3"><input type="text" id="output" value="0"></td> <td><button value="C" onClick="calc('C')">C</button></td> </tr> <tr> <td><button onClick="calc('7')">7</button></td> <td><button onClick="calc('8')">8</button></td> <td><button onClick="calc('9')">9</button></td> <td><button onClick="calc('/')">/</button></td> </tr> <tr> <td><button onClick="calc('4')">4</button></td> <td><button onClick="calc('5')">5</button></td> <td><button onClick="calc('6')">6</button></td> <td><button onClick="calc('*')">*</button></td> </tr> <tr> <td><button onClick="calc('1')">1</button></td> <td><button onClick="calc('2')">2</button></td> <td><button onClick="calc('3')">3</button></td> <td><button onClick="calc('-')">-</button></td> </tr> <tr> <td><button onClick="calc('0')">0</button></td> <td><button onClick="calc('.')">.</button></td> <td><button onClick="calc('+')">+</button></td> <td><button onClick="calc('=')">=</button></td> </tr> </table> </body> </html>script.jsfunction calc(cmd){ const element = document.getElementById('output') const value = element.value if(cmd === '='){ element.value = eval(value) }else if(cmd === 'C'){ element.value = '0' }else if(value === '0') { element.value = cmd }else{ element.value += cmd } }Vue.jsの場合
上記実装において、
button
タグの羅列が冗長であるため、Vue.jsで書き直してみる。
v-forを使うことで、繰り返し処理を記述できる。とりあえず、動かしたい人は、cloneして下さい。
clonegit clone https://github.com/Naoto92X82V99/vue-calc.gitindex.html<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <table id="app"> <tr> <td colspan="3"><input type="text" v-model="output"></td> <td><button value="C" v-on:click="calc('C')">C</button></td> </tr> <tr v-for="row in items"> <td v-for="item in row"> <button v-on:click="calc(item)">{{ item }}</button> </td> </tr> </table> <script src="script.js"></script> </body> </html>script.jsvar app = new Vue({ el: '#app', data: { output: '0', items: [ ['7', '8', '9', '/'], ['4', '5', '6', '*'], ['1', '2', '3', '-'], ['0', '.', '+', '='] ] }, methods: { calc: function (cmd) { if(cmd === '='){ this.output = eval(this.output) }else if(cmd === 'C'){ this.output = '0' }else if(this.output === '0') { this.output = cmd }else{ this.output += cmd } } } })まとめ
Vue.jsを用いて、最小構成でeval電卓を実装しました。
間違い・指摘等があればコメントお願いします。参考文献
- 投稿日:2019-02-09T12:10:40+09:00
SlickGrid で遊んでみる
SlickGridとは
Excelみたいなのを作るのに便利なJavaScriptライブラリ
https://github.com/mleibman/SlickGrid/wiki詳細はこの辺
https://qiita.com/icoxfog417/items/98e34c0555991033afec何をしたかったのか
Excelで設定データを入力していたら1000超えたあたりで重い上に面倒になったのでいっそのこと自作してしまおうと思った。
GUIベースだとMacとWindowsを行き来する今の環境だと面倒なのでブラウザベースにした。仕様
- データの更新を検出したらデータベースを更新(データをJSONにしてAPIを叩く)
- データの更新は編集行が変わった時だけに行う(更新頻度が高いと負荷がかかるため)
- データの更新はセルを選択すれば可能
- UpdateかInsertかはサーバー側で処理する(update APIで行う)
- データベースはsqlite3を選択(検索APIを考慮するとcsvよりデータベースを使った方が良いがこの程度でサーバーを起動するのが面倒)
- エラーメッセージはコンソールログに出し、アラートではださない
- 強制更新ボタンは今回は付けていない(関数呼び出すだけなので実装自体は楽)
- 絞り込みが可能になっている(search APIで行う)
- サーバーにはPythonの簡易サーバー(CGIHTTPServer)を使用(クライアントPC1台でも完結させたいため)
- 見栄えはcssで後からどうにかする
- データもコードもクラウドストレージと同期させる(データの管理を楽にするため)
実装
SAMPLEを利用する
一から書くと大変なのであちこちからサンプルを参考(コピペしてカスタマイズ)して使っていますが、どこから持ってきたのか覚えて居ない……。
完成したのがこんなコード(cssはフォームの表示を整える為にあります)
※元のライセンスがMITライセンスなのでMITライセンスになると思います。
HTML
wordedit.html<!DOCTYPE HTML> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <title>編集</title> <link rel="stylesheet" href="./lib/slickgird/slick.grid.css" type="text/css"/> <link rel="stylesheet" href="./lib/slickgird/css/smoothness/jquery-ui-1.8.16.custom.css" type="text/css"/> <link rel="stylesheet" href="./serch.css" type="text/css"> <!--- <link rel="stylesheet" href="examples.css" type="text/css"/> --> <!-- from SlickGrid example Spread Sheet --> <style> .slick-cell.copied { background: blue; background: rgba(0, 0, 255, 0.2); } </style> </head> <body> <div id = "header"> <div class = "header_text">絞り込み:<input type="text" id="word" class="textbox" size="10" onkeydown="CreateSpreadSheet(document.getElementById('word').value);"></div> </div> <div style="position:relative 1;top: 200px;"> <div style="width:100%;"> <div id="myGrid" style="height:800px;"></div> </div> <script src="./lib/slickgird/lib/firebugx.js"></script> <script src="./lib/slickgird/lib/jquery-1.7.min.js"></script> <script src="./lib/slickgird/lib/jquery-ui-1.8.16.custom.min.js"></script> <script src="./lib/slickgird/lib/jquery.event.drag-2.2.js"></script> <script src="./lib/slickgird/slick.core.js"></script> <script src="./lib/slickgird/plugins/slick.autotooltips.js"></script> <script src="./lib/slickgird/plugins/slick.cellrangedecorator.js"></script> <script src="./lib/slickgird/plugins/slick.cellrangeselector.js"></script> <script src="./lib/slickgird/plugins/slick.cellcopymanager.js"></script> <script src="./lib/slickgird/plugins/slick.cellselectionmodel.js"></script> <script src="./lib/slickgird/slick.editors.js"></script> <script src="./lib/slickgird/slick.grid.js"></script> <script> //検索APIの指定 const searchapi = './api/v1/search'; //更新APIの指定 const updateapi = './api/v1/update'; var grid; var data = []; var options = { editable: true, enableAddRow: true, enableCellNavigation: true, asyncEditorLoading: true, autoEdit: true }; var columns = [ { id: "selector", name: "", field: "num", width: 60 } ]; fieldnames = ['単語','読み','コメント']; //フィールド名を指定 fieldids = ['word','read','comment']; //フィールドIDを指定 //上二つの数は一致すること for (var i = 0; i < fieldnames.length; i++) { columns.push({ id: fieldids[i], name: fieldnames[i], field: fieldids[i], width: 120, editor: FormulaEditor }); } /*** * A proof-of-concept cell editor with Excel-like range selection and insertion. */ function FormulaEditor(args) { var _self = this; var _editor = new Slick.Editors.Text(args); var _selector; $.extend(this, _editor); function init() { // register a plugin to select a range and append it to the textbox // since events are fired in reverse order (most recently added are executed first), // this will override other plugins like moverows or selection model and will // not require the grid to not be in the edit mode _selector = new Slick.CellRangeSelector(); _selector.onCellRangeSelected.subscribe(_self.handleCellRangeSelected); args.grid.registerPlugin(_selector); } this.destroy = function () { _selector.onCellRangeSelected.unsubscribe(_self.handleCellRangeSelected); grid.unregisterPlugin(_selector); _editor.destroy(); }; this.handleCellRangeSelected = function (e, args) { _editor.setValue( _editor.getValue() + grid.getColumns()[args.range.fromCell].name + args.range.fromRow + ":" + grid.getColumns()[args.range.toCell].name + args.range.toRow ); }; init(); } $(function () { CreateSpreadSheet('all'); }); function CreateSpreadSheet(opt){ let datacount = 0; if(opt == 'all' || opt == '') { opt = 'all=1'; } else { opt = 'word=' + opt; } $.getJSON(searchapi,opt,function(data, status, xhr) { datacount = data.length; for( let i = 1 ;i <= data.length; i++){ let d = (data[i-1]); d["num"] = i; } grid = new Slick.Grid("#myGrid", data, columns, options); grid.setSelectionModel(new Slick.CellSelectionModel()); grid.registerPlugin(new Slick.AutoTooltips()); // set keyboard focus on the grid grid.getCanvasNode().focus(); var copyManager = new Slick.CellCopyManager(); grid.registerPlugin(copyManager); copyManager.onPasteCells.subscribe(function (e, args) { if (args.from.length !== 1 || args.to.length !== 1) { throw "This implementation only supports single range copy and paste operations"; } var from = args.from[0]; var to = args.to[0]; var val; for (var i = 0; i <= from.toRow - from.fromRow; i++) { for (var j = 0; j <= from.toCell - from.fromCell; j++) { if (i <= to.toRow - to.fromRow && j <= to.toCell - to.fromCell) { val = data[from.fromRow + i][columns[from.fromCell + j].field]; data[to.fromRow + i][columns[to.fromCell + j].field] = val; grid.invalidateRow(to.fromRow + i); } } } grid.render(); }); grid.onAddNewRow.subscribe(function (e, args) { var item = args.item; var column = args.column; grid.invalidateRow(data.length); data.push(item); grid.updateRowCount(); grid.render(); }); let beforeRow = -1; let beforeRowdata = {}; grid.onSelectedRowsChanged.subscribe(function (e, args) { rownum = args.rows[0]; if(beforeRow >= 0 && beforeRow != rownum){ //編集行が変わったチェック console.log(beforeRow); rowdata = grid.getDataItem(beforeRow); let a = JSON.stringify(beforeRowdata); // 編集前データ let b = JSON.stringify(rowdata); // 編集後データ if(a !== b ){ // 変更があるかチェック console.log('edited!'); // 更新APIを呼び出す //サンプルではJOINEDキーを使っているので更新前のデータを二つ送っている rowdata["oldword"] = beforeRowdata["word"]; rowdata["oldread"] = beforeRowdata["read"]; let params = JSON.stringify(rowdata); console.log(params); //Application/JSONで送りたかったのですがPythonのCGIServerだと上手く取得できる方法が分からなかったのでのでx-www-form-urlencodedのdata='jsondata'でPOSTしています。 $.ajax({ type: 'POST', // method = "POST" url: updateapi, // POST送信先のURL data: "data=" + params, // JSONデータ本体 contentType: 'application/x-www-form-urlencoded', // リクエストの Content-Type dataType: 'text', success: function(json_data) { // 200 OK時 console.log(json_data); }, error: function(json_data) { console.log(json_data); console.log('server error'); }, complete: function() { // 成功・失敗に関わらず通信が終了した際の処理 } }); } $.extend(true,beforeRowdata,grid.getDataItem(rownum)); } else if(beforeRow < 0){ $.extend(true,beforeRowdata,grid.getDataItem(rownum)); } beforeRow = rownum; }); }); } </script> </body> </html>Update APIとSearch APIを作成
コードがあまりに汚い。
テーブルスキーマ
drop table WordDictionary; create table WordDictionary( WORD text not null, READ text not null, comment text, UNIQUE (WORD,READ) );Search API(JSONで検索結果を返す)
search#!/usr/local/bin/python3 import urllib import cgitb import sqlite3 import json from contextlib import closing import sys import configparser import os import os.path as path #config.iniにはデータベースファイルの位置が入っている #実行環境によって相対パスでは取得できないのでsearch APIのフォルダの場所を自動取得 #config.ini #[setting] #database=/path/too inipath = path.join(path.dirname(sys.argv[0]),'config.ini') inifile = configparser.ConfigParser() inifile.read( inipath , 'UTF-8') dbname = inifile.get('settings', 'database') #cgitb.enable() def query4Yomi(conn,yomi): cur = conn.cursor() yomi = '%' + yomi + '%' return cur.execute('select * from WordDictionary where READ like ?', (yomi,)) def query4Word(conn,word): cur = conn.cursor() return cur.execute('select * from WordDictionary where WORD = ?', (word,)) def queryall(conn): cur = conn.cursor() return cur.execute('select * from WordDictionary order by READ') def query4Any(conn,word): cur = conn.cursor() word = '%' + word + '%' return cur.execute('select * from WordDictionary where READ like ? or WORD like ? or comment like ? order by READ', (word,word,word,)) with closing(sqlite3.connect(dbname)) as conn: if 'QUERY_STRING' in os.environ: query = urllib.parse.parse_qs(os.environ['QUERY_STRING']) else: query = {} if ( 'word' in query ): # results = query4Yomi(conn,query['word'][0]) results = query4Any(conn,query['word'][0]) elif ( 'all' in query ): results = queryall(conn) else: results = {} print ("Content-Type: application/json") print ("") print ('[') line = 0 for row in results: if (line !=0): print (',') print ('{') print ('"word": "' + row[0] + '"') print (',"read": "' + row[1] + '"') print (',"comment": "' + row[2] + '"') print ('}') line += 1 print (']')Update API(data='JSONDATA'でJSONDATAの内容をデータベースにアップ
update#!/usr/local/bin/python3 import cgi import cgitb import sqlite3 import sys import json import configparser import os import os.path as path #config.iniにはデータベースファイルの位置が入っている #実行環境によって相対パスでは取得できないのでupdate APIのフォルダの場所を自動取得 #config.ini #[setting] #database=/path/too inipath = path.join(path.dirname(sys.argv[0]),'config.ini') inifile = configparser.ConfigParser() inifile.read( inipath , 'UTF-8') dbname = inifile.get('settings', 'database') from contextlib import closing def update(conn,param): cur = conn.cursor() cur.execute('update WordDictionary ' + 'SET WORD = ?, READ = ?, comment = ? WHERE WORD = ? and READ = ?', (param['word'],param['read'],param['comment'],param['oldword'],param['oldread'])) conn.commit() def query(conn,word,read): cur = conn.cursor() cur.execute('select WORD,READ from WordDictionary where WORD = ? and READ = ?', (word,read)) s = cur.fetchone() try: if (str(s[0]) == word and str(s[1]) == read): return (1,s) else: return (0,None) except: pass return (0,None) def insert(conn,param): cur = conn.cursor() p ={'comment': ''} for key in p.keys(): if(key in param): p[key] = param[key] cur.execute('insert INTO WordDictionary ' + 'VALUES (?, ?, ?)', (param['word'],param['read'],p['comment']) conn.commit() with closing(sqlite3.connect(dbname)) as conn: print ("Content-Type: application/json") print ("") # if (os.environ['REQUEST_METHOD'] != "POST"): # print (str(os.environ['REQUEST_METHOD'])) form = cgi.FieldStorage() q = json.loads(form['data'].value) try: (r,s0) = query(conn,q['oldword'],q['oldread']) if(r != 1) : insert(conn,q) m = 'insert' else: update(conn,q) m = 'update' (r,s1) = query(conn,q['word'],q['read']) results = {"result" : "200" , "method" : m} print (results) except: import traceback as tr s = tr.format_exc() results = {"result" : "403" ,"trace" : s} print (results)簡易サーバー
cgiserver#!/usr/local/bin/python3 # -*- coding: utf-8 -*- import http.server import threading import webbrowser import time handler = http.server.CGIHTTPRequestHandler handler.cgi_directories = ['/cgi-bin','/api'] http.server.test(HandlerClass=handler)フォームも起動させようとするやつ
cgiserver.py#!/usr/local/bin/python3 # -*- coding: utf-8 -*- import http.server import threading import webbrowser import time url = 'http://localhost:8000/wordedit.html' def openBrowser(url): time.sleep(1) print ("OPEN URL {}".format(url)) webbrowser.open(url) #1秒待ってからブラウザを起動 try: if __name__ == "__main__": thread = threading.Thread(target=openBrowser,args=(url,)) thread.start() except: print ("Error: unable to start thread") handler = http.server.CGIHTTPRequestHandler handler.cgi_directories = ['/cgi-bin','/api'] http.server.test(HandlerClass=handler)補足:Windowsでは起動前に以下の環境変数を設定しておかないと挙動不審になるので注意
set PYTHONIOENCODING=utf-8結果
Excelより軽くて便利
課題
- 必要なJSがどれか全く分かっていない
- テーブルのメタデータからフィールド名を自動生成&自動処理可能にする(汎用化)
- 動作環境が変わった時に設定ファイルをいじらなくても動くようにする
- JavaScriptのコードをHTMLから分離
- 新規追加に少しトリッキーな操作を要求する(データベースでエラーが出た場合の処理が甘い)
- 一括更新
- 投稿日:2019-02-09T09:47:39+09:00
Element から学ぶ Vue.js の component の作り方 その2 (button)
button
前提
Elemnt 2.52
公式ページ
https://element.eleme.io/#/en-USボタン
用意されている機能
- サイズの指定 3種
- タイプの指定によるデザインテーマの指定
- ボタンの形式(プレーン、角丸、円形)
- ローディングアクション
- 非活性
- アイコンの利用
- オートフォーカス
- ネイティブタイプ(button / submit / reset)
構成
<button class="el-button" @click="handleClick" :disabled="buttonDisabled || loading" :autofocus="autofocus" :type="nativeType" :class="[ type ? 'el-button--' + type : '', buttonSize ? 'el-button--' + buttonSize : '', { 'is-disabled': buttonDisabled, 'is-loading': loading, 'is-plain': plain, 'is-round': round, 'is-circle': circle } ]" >disable は buttonDisabled と loadng のいずれか。
buttonDisabled は下記の computed で定義。
親から渡した prop か、elForm を inject しているのでフォームの中においてるとフォームと連動する。buttonDisabled() { return this.disabled || (this.elForm || {}).disabled; }他は prop の値を素直に受け取っている。
<i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-if="icon && !loading"></i> <span v-if="$slots.default"><slot></slot></span>loading が true の場合は loading icon が表示される。
icon が指定されていて、loading 中でなければ icon が表示される。
slot の指定が可能になっている。その他の computed
computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, buttonSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; },buttonSize は prop か _elFormItemSize の帰り値の inject している elForm か elFormItems のサイズ、
あと、this.$ELEMENT は何だろう。グローバルな規定があるのかな。※ 要調査所感
第二回目だが、一回目と比べてあまり成長がないな。。
次こそ頑張ろう。
- 投稿日:2019-02-09T09:30:52+09:00
[chrome拡張機能] chrome.cookies APIの使い方おぼえがき
クッキー関係のプラグインを作りました
SUGOI!Cookies: gclid tester for Google Ads
https://chrome.google.com/webstore/detail/sugoicookies-gclid-tester/oidgodfancakeifokbiocnnlfoocmbpd?hl=jaこんな拡張機能作ってみました。
幸せになる人:
・今までgclidテストを手打ちでやってた人
・今までわざわざF12 => Application => Cookiesで
Goolge広告やアナリティクスのクッキーを確認していた人超ニッチです(笑)。せっかくなので、chrome.cookies APIに関してメモ。
前提
①: 公式のドキュメントはここです。
Chrome API => https://developer.chrome.com/extensions/cookies
MDN => https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/API/cookies②: このAPIは、background.jsでしか呼べない。
なので、content.jsからは、
chrome.runtime.sendMessage で「このAPI呼んで!」と、
叫ぶ必要があります。例えば、下のような感じ。content.js/** * to background.js */ const getCookies_ = () =>{ chrome.runtime.sendMessage({message:'getCookies', domain:document.domain}, function callback); };background.js/** * eventListener * chrome.cookies should be called in this file */ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { const domain = request.domain; const msg = request.message; if(msg=='getCookies'){ chrome.cookies.get... } });③: manifest.jsonで「このURLのクッキー触るよ」の宣言が必要
manifest.json{ "permissions": [ "cookies", "<all_urls>" ] }API
onChanged : クッキーに変更が加えられてた時に呼ばれます
onChanged.addEventListener
/** * listen to change events to cookies */ chrome.cookies.onChanged.addListener((e)=>{ /** @type{string} 'expired', 'explicit' or 'override' */ console.log(e.cause)); /** @type{Object} */ console.log(e.cookie)); const cookie = e.cookie; cookie.url /** @type string*/ // manifest.jsonで該当URLのpermissionが必要です。 cookie.name /** @type string*/ // クッキーは"name=val"形式。name部分です。 cookie.value /** @type string*/ // "name=val" のval cookie.domain /** @type string*/ cookie.path /** @type string*/ cookie.secure /** @type boolean*/ cookie.httpOnly /** @type boolean*/ cookie.SameSiteStatus /** @type Enum "no_restriction", "lax", or "strict" */ cookie.expirationDate /** @type double*/ cookie.storeId /** @type string*/ /** @type{boolean} if the change event has got triggered by "removing a cookie" */ console.log(e.removed)); });onChanged.removeEventListener: イベントリスナーの削除
/** * eventListener */ chrome.cookies.onChanged.addListener(doSomething_); /** * remove the event added by above */ chrome.cookies.onChanged.removeListener(doSomething_); /** * @private * @param {Object} event - you can access to cookie obj by "event.cookie" */ function doSomething_(event){ // do something };getAll : 空オブジェクトを渡せば本当に全部のクッキーが。ドメイン渡せば、そのドメインのクッキーがリターン。
chrome.cookies.getAll({},((allCookies)=>{ doSomething_(allCookies); })); chrome.cookies.getAll({domain:'qiita.com'},((qiitaCookies)=>{ doSomething_(qiitaCookies); }));get: url, nameを指定して、クッキーを1つゲット
chrome.cookies.get({url:'aUrl', name:'aCookieName'}, ((aCookie)=>{ doSomething_(aCookie); }));remove: url, nameを指定して、クッキーを1つ削除
chrome.cookies.remove({url:'aUrl', name:'aCookieName'}, function callback);set: クッキーを生成します。
/** * Objectの中にurlプロパティは必須です。 */ chrome.cookies.set(object details, function callback);番外編
background.jsではなく、content.jsでやりたいならdocument.cookieでやりましょう
https://developer.mozilla.org/ja/docs/Web/API/Document/cookie//そのドメインのクッキーが、stringで入る let cookies = document.cookie; // Array.<string> - 'name=value'の配列 cookies = cookies.split(';'); // 新しいクッキーを生成 document.cookie = 'aNewName=aNewVal';以上です。最後までありがとうございました!
- 投稿日:2019-02-09T01:43:15+09:00
配列を指定サイズで分割
ライブラリ使わずにやる
Array.reduce()
、spread operator,Array.splice()
を組み合わせるconst arr = [...Array(100).keys()]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8,...,99]; const size = 8; const result = arr.reduce( (prev, curr, index) => index !== 0 && index % size !== 0 ? [...prev.splice(0, prev.length - 1), [...prev[prev.length - 1], curr]] : [...prev, [curr]], [] ); //=> [ // [0, 1, 2, 3, 4, 5, 6, 7], // [8, 9, 10, 11, 12, 13, 14, 15], // [16, 17, 18, 19, 20, 21, 22, 23], // [24, 25, 26, 27, 28, 29, 30, 31], // [32, 33, 34, 35, 36, 37, 38, 39], // [40, 41, 42, 43, 44, 45, 46, 47], // [48, 49, 50, 51, 52, 53, 54, 55], // [56, 57, 58, 59, 60, 61, 62, 63], // [64, 65, 66, 67, 68, 69, 70, 71], // [72, 73, 74, 75, 76, 77, 78, 79], // [80, 81, 82, 83, 84, 85, 86, 87], // [88, 89, 90, 91, 92, 93, 94, 95], // [96, 97, 98, 99] // ];lodash使える環境
const arr = [...Array(100).keys()]; const size = 8 const result = _(arr).chunk(size).value()