akatak’s blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学しています。自らの覚え書きや成果物、感じたことなどを脈絡もなく書き連ねるブログです。

React HooksとDjango REST Frameworkを使ってTo Do Listを作ってみた(後編)〜バックエンド編

前回に引き続き、React(フロントエンド)+Django(バックエンド)によるTo Do Listアプリを作っていきます。

今回はバックエンド編です。

環境構築

Djangoを利用しますので、任意の名前の仮想環境を構築しておきます。

当該仮想環境を立ち上げて、以下をインストールします。

pip install django
pip install djangorestframework
pip install django-cors-headers

djangorestframeworkDjango REST Frameworkを利用するためのもので、django-cors-headersは、ReactアプリのアドレスとDjangoのアドレスが異なっているため、通常ではブラウザは通信できません。ReactアプリとDjangoが適切にAPI通信を行うために必要なモジュールとなります。

私の環境における各バージョンは以下の通りです。

Django             3.0
djangorestframework 3.11.1
django-cors-headers 3.4.0

Djangoの設定

Django プロジェクト及びアプリの作成

まずは、Djangoプロジェクトを作成します。任意のプロジェクト名をつけますが、ここではtodoprojとしています。

そしてtodoprojフォルダに移動し、そこでdjangoアプリを作成します。ここではアプリ名をtodoappとしています。

django-admin startproject todoproj
cd todoproj
python manage.py startapp todoapp

settings.pyの設定

todoprojフォルダ傘下のsettings.pyを以下の通り修正します。

INSTALLED_APPS = [
    ...
    'rest_framework', # <- 追加
    'corsheaders', # <- 追加
 'todoapp.apps.TodoappConfig' # <- todoappを追加
]

## django-cors-headersを利用する場合
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', 
    'django.middleware.common.CommonMiddleware',
    .....
]

# Djangoが通信する相手であるReactアプリのポートを指定
CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000'
]

Djangoアプリの書き方

それでは、Django REST Frameworkを利用したアプリを具体的に記述していきます。

モデル(models.py)

Django REST Frameworkを利用する場合であっても、このモデルの作成は通常のDjangoと同様となります。したがって、models.pyにdjango.db.models.Modelを継承したクラスとして定義します。

Reactアプリで使っているfieldをここで定義します。idはReact側でuuidを自動的に付与しますので、ここではCharField(文字列)とします。また、primary_key=Trueとしないとmakemigration時に警告が出てきます。その他titleを文字列としてisCompletedをBool値として設定します。

rom django.db import models

class Task(models.Model):
    id = models.CharField(max_length=128, primary_key=True)
    title = models.CharField(max_length=256)
    isCompleted = models.BooleanField(default=False)

    def __str__(self):
        return self.title

リアライザ(serializers.py)

リアライザは、データを保持しておくための入れ物で、JSON文字列とモデルオブジェクトの相互変換をしてくれるものです(「現場で使えるDjango REST Frameworkの教科書」)。

今回は、Taskモデルでの定義に基づき、JSONの入出力が行われるため、ModelSerializerを継承したシリアライザを利用することができます。これにより、モデルのフィールド定義が内部的に再利用されるため、このsirializers.pyでの記述が非常に簡単になります。

具体的には以下の通り。

from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ['id', 'title', 'isCompleted']

ビュー(views.py)

このビューの役割は、JSONデータが入ったリクエストオブジェクトを受け取り、APIの種類に応じた処理を実行し、JSON形式のレスポンスオブジェクトを返すことです。

今回のモデルは、Taskのみの単一モデルなので、ModelViewSetを利用すれば、CRUDを処理するAPIを簡単に実装することができます。

具体的には、views.pyを以下のとおり最低限記述するだけで利用可能ととなります。

from rest_framework import viewsets
from .serializers import TaskSerializer
from .models import Task

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

URLconf(Urls.py)

URLconfとはURLのパターンの集まりで、適切なビューを見つけるために、DjangoがリクエストされたURLと照合するものです。

API用のURLパターンを新たに設定する場合、通常のDjangoと同様にurlpatternsリストにpath関数等を利用して、URLパターンとビューのセットを記載します。

# todoproj/settings.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todoapp.urls'))

なお、ModelViewSetを継承した場合には、Django REST Framework独自のRouterクラスを使って設定します。

# todoapp/settings.py

from django.urls import path, include
from rest_framework import routers
from .views import TaskViewSet

router = routers.DefaultRouter()
router.register('tasks', TaskViewSet)

urlpatterns = [
    path('', include(router.urls))
]

これでdjangoを起動して、ブラウザのアドレスバーにhttp://127.0.0.1:8000/api/tasksを入力すると、ブラウザ上にtasks(task一覧)が表示されるようになります。

なお、DjangoのAdmin管理画面でTaskの登録状況が一応見られるように、以下のとおり登録しておきます。

from django.contrib import admin
from .models import Task

admin.register(Task)

以上で準備は完了です。

それでは、仮想環境になっていることを確認して、python manage.py rumserverDjangoを起動させます。

そしてアドレスバーにhttp://127.0.0.1:8000/api/tasksを入力すると以下の画面が立ち上がりました。まだ、データは登録していないので、ブランクリストが表示されていますね。

f:id:akatak:20200901160822p:plain

データをいくつか登録しておきましょう。

下側のhtml formの記載のあるボックスに「id」「title」「isCompleted」が入力できるようになっているので、適当に入力してPOSTを押すと登録できます。

idは文字列で他のtaskと重ならないように以下のとおり入力してみました。

id: a001
title: Reactを学習する
isCompleted: チェック

id: a002
title: お米を買う
isCompleted: チェックしない

id: a003
title: お酒を買う
isCompleted: チェックをしない

上段のボックスのGET をクリックします。すると以下のとおり、Task Listが表示されました。

f:id:akatak:20200901162138p:plain

なお、入力済みの個別taskの修正や削除を行いたい場合は、アドレスバーに http://127.0.0.1:8000/api/tasks/a001と最後にidを入力します。すると以下のとおり、個別明細とPUTDELETE表示が出ますので、適宜修正や削除が可能です。

f:id:akatak:20200901162541p:plain

ここでは、「学習する」を「勉強する」に修正しておきました。

Reactアプリと繋げる

さて、いよいよReactアプリとDjangoを接続します。まずはReactアプリを修正して、json-serverではなく、Djangoに接続できるように設定します。

Reactアプリの修正

baseUrlを以下のとおり修正します。

細かいところですが、addの場合とupdateの場合には、${baseUrl}/${baseUrl}/${id}/と最後に/をつけないとエラーとなってしまいます。

import axios from 'axios';

// const baseUrl = "http://localhost:3001/tasks"; 
const baseUrl = "http://localhost:8000/api/tasks";

async function getAll () {
  const response = await axios.get(baseUrl);
  return response.data;
}

async function add (newTask) {
  const response = await axios.post(`${baseUrl}/`, newTask)
  return response.data;
}

async function update(id, updatedTask) {
  const response = await axios.put(`${baseUrl}/${id}/`, updatedTask)
  return response.data;
}

async function _delete(id) {
  await axios.delete(`${baseUrl}/${id}`);
  return id;
}

export default { getAll, add, update, delete: _delete }

これで準備が整いました。

React アプリのあるフォルダ(ここではmytodo)に移動し、npm startでアプリを立ち上げます。

f:id:akatak:20200901164029p:plain

無事にDjangoと接続できました。


React-to-do-sample

djangoプロジェクトのtodoprojの下にfrontendというフォルダを作成し、その下にmytodoを移動しておくと、djangoとの関係性が明確で良いかと思います。

今回は単純なアプリでしたが、それでもHooksの概念がなかなか理解できず(副作用って一体何?だったり、Hooksの種類がいろいろあったりで)、手間取ったところもありましたが、何とかイメージしたことは実装できました。

今後は、ユーザー登録機能だったり、順番を入れ替える、期日管理を行うなど実装にチャレンジしたいなとは思っています。ただ、やりたいことが結構増えてしまっていますので、いつになるか分かりませんが...

React HooksとDjango REST Frameworkを使ってTo Do Listを作ってみた(前編)〜フロントエンド編

最近気に入って使っているDashなんですが、React.jsというJavaScriptのライブラリをベースにしていると開発元のホームページに書いてあります。

また、Dashのグラフコンポーネントを新たに作るならReactを学ぶことが勧められています。

さらに、こちらのページによるとJavaScriptのフロントエンドフレームワークの中で最も世界的に人気があるようで、Googleトレンドでも同様の結果となっています。

2019.stateofjs.com

これらにそそのかされて、この1ヶ月程、Reactを集中的に学んでみました(JavaScriptの再学習も併せて)。そこで、自分自身の理解を深めるために、ReactでTo Do Listアプリを作成してみましたので、紹介したいと思います。

なお、Djangoをバックエンドとして使いたいと思っていろいろなサイトを見てみましたが、Django REST Frameworkを使っているサイトが多いようなので、今回Django REST Frameworkを利用することにしました。

出来上がりはこんな感じ。


React-to-do-sample

まずは、フロントエンドを、Reactを使って、To Do LIstアプリを作成(データベースとしてはjson-serverを利用)。その後、Django REST Frameworkを使ってバックエンドを作成し、一旦作成したReactと繋げていきます。

Reactのインストール(Mac

1. node.jsをインストール

  こちらのサイトからNode.jsのLTS版をインストールします。 nodejs.org

Node.jsJavaScriptで作られているバックエンドのフレームワークです。今回はNode.jsをインストールすると、同時にインストールされるnpm(Node Package Manager: Node.jsのパッケージを管理するツール)を利用します。

2. 新たにReactアプリを作成

1により Node >= 8.10 及び npm >= 5.6 がインストールされているので、新たにReactプロジェクトを作成するには以下をターミナルから実行するだけです。mytodoのところは任意のアプリ名を入れます。

npx create-react-app mytodo
cd mytodo
npm start

ここでReactアプリが立ち上がるはずが、以下のような表示が出て先に進めなくなってしまうことが環境によっては良くあります。私の場合がそうです。

f:id:akatak:20200830143410p:plain

この場合は一旦ctr+cにて中断して、ターミナルにて

unset HOST

を入力し、Enterキーを押します(HOST情報の解除)。これで再度npm startにてReactアプリ(初期画面)が立ち上がるようになります。

必要なパッケージのインストール

今回インストールするパッケージとその役割は以下の通り。
- json-server:Reactから利用できる簡易サーバー(モックアップサーバーというらしい)です。まずはフロントエンドで完結できるよう、このサーバーとjsonデータをやりとりできるようにします。
- axios:モックアップサーバーとの通信に利用する非同期通信ライブラリです。
- uuid:一意なIDを生成するライブラリで、各TaskのIDの作成に利用します。

インストールは、先ほどのmytodo直下で以下のコマンドを入力して行います。

npm install --save-dev json-server
npm install axis
npm install uuid

モックアップサーバーの設定

初期データの準備

mytodoフォルダ直下にdb.jsonというJSONファイルを追加し、以下を記述します。

{
  "tasks": [
    {
      "id": "f543f73d-48d6-4b65-a07f-20cf68e11461",
      "title": "TO DO LISTをReactで作成する",
      "isCompleted": false
    },
    {
      "id": "9e25ddb3-ac4a-4354-9320-d8211762d1fb",
      "title": "お米を買う",
      "isCompleted": false
    },
    {
      "id": "6a18845a-c537-4263-8cc7-9e62b96d90ee",
      "title": "お酒を買う",
      "isCompleted": false
    }
  ]
}

これがモックアップサーバー上のデータになります。idは取り敢えず何でも大丈夫です。新規で追加される場合には、uuidにより自動的にidが付加されます。

モックアップサーバーの起動準備

mytodoフォルダ直下にあるpackage.jsonの中にあるscriptsセクションに以下のとおり、json-serverに関する設定を追加します。

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server --watch db.json --port 3001" // 追加
  },

これにより、ターミナルからnpm run serverというコマンドを入力すれば、モックアップサーバーが立ち上がります。なお、npm startで立ち上がるReactアプリのポートが3000のため、モックアップサーバーのポートを--port 3001と3001に設定しています。--watch db.jsonにてjson-serverが常にdb.jsonの変更を監視できるように設定しています。

モックアップサーバーの起動確認

ターミナルにてnpm run serverと入力するとモックアップサーバーが立ち上がります。ここで、任意のブラウザにてhttp://localhost:3001/tasksにアクセスすると画面にdb.jsonに登録したデータが表示されます。

To Do Listアプリの作成

こちらのサイトなどを参考にしながらTo Do Listアプリを作成しました。

dev.to

ファイル構成

ファイル構成は以下の通りです。npx create-react-appでインストールされるファイルで不要なものは削除しています。

public/
            index.html
            style.css
src/
           index.js
           App.js
           api/
                  TaskApi.js
           components/
                                 Task.js
                                 TaskForm.js
                                 TaskList.js
                                 TaskProvider.js
node_modules/
db.json
package.json
package-lock.json

ファイルの内容

このアプリでは、React Hooksのうち、基本フックであるuseState, useContext及びuseEffectを利用しています。

useStateは基本のHookになります。Reactのオフィシャルページによると以下のとおり。

useState は現在の state の値と、それを更新するための関数とをペアにして返します。この関数はイベントハンドラやその他の場所から呼び出すことができます。

要は、コンポーネントの中でデータを保持し、関数を使ってそのデータを加工できる仕組みをいいます。

また、useContextは、オフィシャルページでは以下のとおり。

コンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返します。コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider> の value の値によって決定されます。

何を言っているか良くわかりませんねぇ。

いろいろ調べてみると、このフックを利用すれば、親コンポーネントから子コンポーネントにデータを渡す際に使うPropsを利用しなくても良くなります。加えて「親から子にPropsを渡し、さらにその子供にPropsを渡し、さらにその子供に...」といったPropsのバケツリレーを行う必要がなくなり、どのコンポーネントからでもデータにアクセスできるようにするためのフックです。

useEffectは、オフィシャルページは分かりにくいので、以下のとおり整理しました。

コンポーネントに副作用のある命令型コード(side effect)を追加するフックをいう。 副作用のある命令型コード(side effect)とは次のような処理を実行する関数のこと。これらは関数コンポーネント本体で書くことはできない(バグや非整合性の原因)。
- DOMの変更
- APIとの通信
- 非同期処理
- タイマー
- console.log(ロギング)
代わりに、useEffectを利用する。useEffectに渡された関数は、レンダーの結果が画面に反映された後に動作する(デフォルト)。特定の値が変化した時のみ動作させることも可能。

最初にuseState,useContext, useEffectの全てを利用している本アプリの肝であるTaskProvider.jsについて説明します。

TaskProvider.js

// 1. 各ライブラリから必要なモジュールのインポート
import React, { useState, useEffect, useContext, createContext } from 'react';
import { v4 } from 'uuid';
import TaskApi from '../api/TaskApi';

// 2. Contextオブジェクトとカスタムフック
const TaskContext = createContext();
const useTasks = () => useContext(TaskContext);

function TaskProvider({ children }) {

 // 3. useStateの設定
  const [tasks, setTasks] = useState([]);

 // 4. 当初レンダリング時のデータ取得
  useEffect(() => {
    TaskApi.getAll().then(tasks => {
      setTasks(tasks);
    });
  }, [])

 // 5. タスクの追加
  const addTask = item => {
    const newTask = {
      id: v4(),
      title: item,
      isCompleted: false
    }
    TaskApi.add(newTask).then(addedTask => (
      setTasks([...tasks, addedTask])
    ))
  }

  // 6. データステータスの変更
  const toggleStatus = (id, status) => {
    const item = tasks.find(task => task.id === id)
    const updatedTask = {...item, isCompleted: status}
    TaskApi.update(id, updatedTask).then(newTask => {
      const newTasks = tasks.map(item => item.id === newTask.id ? newTask : item);
      setTasks(newTasks);
    })
  }
   
  // 7. データの削除
  const deleteTask = id => {
    TaskApi.delete(id).then(deletedId => {
      const newTasks = tasks.filter(item => item.id !== deletedId)
      setTasks(newTasks);
    })
  }

 // 8. リターン値
  return (
    <TaskContext.Provider value={{ tasks, addTask, toggleStatus, deleteTask }}>
      { children }
    </TaskContext.Provider>
  )
}

export default TaskProvider;
export { useTasks }

1.各ライブラリから必要なモジュール(関数、オブジェクト等)のインポート

まず最初にReact及び2つのHooks(useStateとuseContext)をインポートします。createContextuseContextを利用するために必要ですので、併せてインポートしておきます。また、新たにTaskを追加した際に、自動的に一意のidを追加するため、uuidライブラリからv4モジュールをインポートします。

2.Contextオブジェクトとカスタムフック

createContextにてContextオブジェクトを生成し、TaskContextとします。このTaskContextを呼び出す(useContext)を関数の形でカスタムフックuseTasksとして定義します。これにより、どのコンポーネントにおいても、tasksアレイや必要な関数(以降で定義)にアクセスできるようになります。

3.useStateの設定

useState Hookにより、taskアレイを初期化し、デフォルト値として空のアレイを設定します。このuseStateは2つのオブジェクトのアレイを返します。1つは状態を表す値(taskアレイ)とその値を変更するのに使う関数(setTasks)です。

4.当初レンダリング時のAPIデータ取得

当初レンダリング時に、モックアップサーバーからAPIによりデータを取得します。APIとの通信は副作用(side-effect)に該当しますので、useEffectを利用する必要があります。第二引数に[]と指定していますので、当初レンダリング時にのみデータを取得します。

データを取得後、setTasks(tasks)にて、tasksアレイに取得データを設定しています。

5.タスクの追加

ここでaddTask関数を作成しており、他のコンポーネントでもタスクを追加することができます。タスクのタイトルを入力すると、v4により自動的にユニークなidと、完了フラグisCompletedfalseに設定されます(newTask)。APIにより、json-serverにnewTaskとして登録した後、setTasksにより、...task(スプレッド構文)で展開される既存taskアレイに、newTaskを追加してtasksに登録します。

6.データステータスの変更

ここでは、toggleStatus関数を作成しています。チェックボックス のオンオフをstatusとして与えた時に、その対象となっているtaskのisCompleteの値(true/false)を変更するようにしています。json-severのデータを書き換えた後に、setTasksによりtaskアレイも変更します。

7.データの削除

ここで、deleteTask関数を作成しています。対象タスクのidを与え、json-serverのデータを削除し、また、taskアレイも変更しています。

8.リターン値

2にて定義したTaskContext<TaskContext.Provider value={{}}>{children}</TaskContext.Provider>の形でchildren (子コンポーネント)を挟んでリターン値として返すことで、コンポーネント間でこのvalueを共有できます。

TaskApi.js

次はTaskApi.jsです。これはaxiosライブラリを利用してjson-serverとデータのやりとりを行うスクリプトファイルです。

GitHub - axios/axios: Promise based HTTP client for the browser and node.js

axiosはエラー発生時のハンドリングのロジック等を追加できますが、ここでは、そうしたロジックを記述せずに単純にaxios.getでデータの取得、axios.postでデータの追加、axios.putでデータの修正、axios.deleteでデータの削除を行っています。

import axios from 'axios';

const baseUrl = "http://localhost:3001/tasks";

async function getAll () {
  const response = await axios.get(baseUrl);
  return response.data;
}

async function add (newTask) {
  const response = await axios.post(baseUrl, newTask)
  return response.data;
}

async function update(id, updatedTask) {
  const response = await axios.put(`${baseUrl}/${id}`, updatedTask)
  return response.data;
}

async function _delete(id) {
  await axios.delete(`${baseUrl}/${id}`);
  return id;
}

export default { getAll, add, update, delete: _delete }

以降は、上位のファイルの内容から順に記載していきます。

index.js

このファイルは、index.htmlの<div id="root"></div>タグに紐つけ、<App />(Appコンポーネント)を表示させる役割を持っています。

<React.StrictMode>は、公式HP↓にも記載されていますが、strictモードの設定で、開発環境のみ有効な機能です。例えば、React Hooksを安全でない使い方をした場合に、警告メッセージが出力されるようになります。

<TaskProvider>は最初に説明したコンポーネントです。この間に挟まれた<App />以降のコンポーネント間でTaskProvider.jsにおいて設定されているvalue'であるtasksアレイ,addTasktoggleTaskdeleteTask間をuseTasks`を通じて共有できるようにしています。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import TaskProvider from './components/TaskProvider';

ReactDOM.render(
  <React.StrictMode>
    <TaskProvider>
      <App />
    </TaskProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

App.js

このファイルは、タイトル(h1タグ)と2つのコンポーネント<TaskForm />及び`から成り立っています。

import React from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';

function App() {
  return (
    <div className="container">
      <h1 className="title">To Do List</h1>
      <TaskForm />
      <TaskList />
    </div>
  );
}

export default App;

TaskList.js

ここでは、useTasks'によりtasksアレイのみ取り出して、map`メソッドによりTaskコンポーネントを展開しています。

import React from 'react';
import Task from './Task';
import { useTasks } from './TaskProvider';

function TaskList() {

  const { tasks } = useTasks();

  return (
    <table>
      <tbody>
        {
          tasks.map((task, id) =>
            <Task key={id} {...task} />
          )
        }
      </tbody>
    </table>
  )
}

export default TaskList;

Task.js

Taskコンポーネントを定義しています。カスタムフックuseTasksを通じて、toggleStatus関数とdeleteTask関数を取り出しています。それぞれチェックボックス のオンオフ、xボタンのクリックに対応させています。また、チェックボックス をオンにすると、titleに取消線(line-through)が表示されるようにしています。

import React from 'react';
import { useTasks } from './TaskProvider';

function Task({ id, title, isCompleted }) {

  const { toggleStatus, deleteTask } = useTasks();

  const checkTask = event => toggleStatus(id, event.target.checked);

  return (
    <tr>
      <td>
        <input type="checkbox" onChange={checkTask} checked={isCompleted} />
      </td>
      <td className="align-left" style={{width: "100%"}}>
        <span style={{textDecoration: isCompleted && "line-through"}}>
          {title}
        </span>
      </td>
      <td>
        <button onClick={() => deleteTask(id)}>x</button>
      </td>
    </tr>
  )
}


export default Task;

TaskForm.js

フォーム入力のためのコンポーネントです。useTasksを通じてaddTask関数を利用できるようにしており、+ボタンを押すとその時点で入力していたタスクが追加されます。

import React, { useState } from 'react';
import { useTasks } from './TaskProvider';

function TaskForm() {

  const [task, setTask] = useState('')
  const { addTask } = useTasks();

  const submit = event => {
    event.preventDefault();
    addTask(task);
    setTask('');
  }

  return (
    <form onSubmit={submit}>
      <input 
        type="text" 
        value={task}
        placeholder=""
        onChange={event => setTask(event.target.value)}
        required
       />
       <button>+</button>
    </form>
  )
}

export default TaskForm

説明は以上です。

今回設定したcssは以下の通りです。

/* style.css */

body {
  background-color: azure;
  min-height: 70vh;
  padding: 1rem;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: center;
  color:rgb(99, 99, 99);
  text-align: center;
  font-size: 100%;
}

.container {
  width: 100%;
  height: auto;
  min-height: 500px;
  max-width: 500px;
  min-width: 350px;
  background: #f1f5f8;
  background-image: radial-gradient(#bfc0c1 7.2%, transparent 0);
  background-size: 25px 25px;
  box-shadow: 4px 3px 7px 2px #00000040;
  padding: 1rem;
  box-sizing: border-box;
}

form input {
  box-sizing: border-box;
  background-color: transparent;
  padding: 0.5rem;
  font-size: 1rem;
  border: 1px solid rgb(185, 185, 185);
  box-shadow: 1px 1px 2px rgb(185, 185, 185) inset;
  width: 80%;
  margin-bottom: 20px;
}

form button {
  color: rgb(245, 245, 245);
  font-size: 1.2rem;
  width: 40px;
  height: 32px;
  border: none;
  background-color: rgb(0, 162, 255);
  border-radius: 5%;
  margin-left: 10px;
  box-shadow: 2px 2px 4px rgb(185, 185, 185);
}

table {
  border-spacing: 0;
}

table td {
  border-bottom: solid 1px rgb(185, 185, 185);
  background-color: white;
  padding: 10px;
}

td button {
  color: rgb(104, 104, 104);
  border: none;
  background-color: rgb(224, 224, 224);
  border-radius: 100%;
}

.title {
  font-family: 'Open Sans', sans-serif;
}

.align-left {
  text-align: left;
}

動かしてみましょう

まず、ターミナルからnpm run serverにてjson-serverを稼働させます。

> mytodo2@0.1.0 server /Users/tak/Desktop/react/mytodo2
> json-server --watch db.json --port 3001


  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3001/tasks

  Home
  http://localhost:3001

  Type s + enter at any time to create a snapshot of the database
  Watching...

うまく立ち上がりました。

それでは別のターミナルからnpm startにてReactアプリを立ち上げます。

f:id:akatak:20200831101350p:plain

うまくいきました。

次回は、django REST frameworkを利用してバックエンドを作成し、今回作成したReactアプリとつなげたいと思います。

DjangoでDashとPlotlyのグラフを同時に描く

今回は、前回の「可視化フレームワークDashをDjangoで利用する」の続編です。Django上に作成したDashと同じページにplotlyで描いたグラフを追加するというものです。

plotly.js CDNを利用できるようにする

まずは、base.htmlにWebページでPlotly.jsを利用できるようにCDNの設定をしておきます。

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>World Map</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<!- ここを追加 ->
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>

views.pyに関数を記述する

前回のviews.pyは以下の通りでした。

# views.py(前回)
def index(request):
    return render(request, 'worldmap/index.html')

今回は、ここに簡単な折れ線グラフを描く関数scatterを記述していきます。

# views.py(今回)
def index(request):
    def scatter():
        x1 = [1, 2, 3, 4]
        y1 = [30, 35, 25, 45]

        trace = go.Scatter(
            x = x1,
            y = y1
        )

        layout = dict(
            title = 'Simple Graph',
            xaxis = dict(range = [min(x1), max(x1)]),
            yaxis = dict(range = [min(y1), max(y1)])
        )

        fig = go.Figure(data=[trace], layout=layout)
        plotly_div = plot(fig, output_type='div', include_plotlyjs=False)
        return plotly_div

    context = {
        'plot': scatter()
    }

    return render(request, 'worldmap/index.html', context=context)

ポイントはplot(fig, output_type='div', include_plotlyjs=False)output_typeinclude_plotlyjsかと思いますが、以下のサイトに詳しいのでそちらをご参照いただければと思います。

Plotlyで作るグラフをDjangoで使う - Qiita

contextにてscatter関数の結果をindex.htmlに渡します。

htmlページに結果を表示する

index.htmlに以下の記述を追記する。

{% extends 'base.html' %}
{% load static %}

{% block content %}

  {% load plotly_dash %}
  <div class="{% plotly_class name='WorldMap' %} card" style="height: 100%; width: 100%;">
    {% plotly_app name='WorldMap' ratio=0.60 %}
  </div>
  <br>
 
  {{ plot | safe}}  <!-  ここを追記 ->


{% endblock %}

あとは、前回 ratio = 1.0と利用可能画面の 100%をDashが占めるように設定していましたが、今回はこれを ratio = 0.65としてplotlyのグラフが表示できるように変更します。

これだけです。

それでは表示してみましょう。

f:id:akatak:20200801085207p:plain

画面下が切れているようですが、スクロールすると表示できます。 うまくいきましたね。

今回は以上です。

ご参考までに以下にスクリプトをアップしています。
GitHub - tak-akashi/djangomap2

可視化フレームワークDashをDjangoで利用する

前回、Dashを利用して世界統計地図を描いてみましたが、これだとシングルページなんですよね。

Dash自体はマルチページにも対応してしてます。が、今後、Webアプリケーションとして機能を拡張していくことを考えたら、DjangoでDashを利用できると良いですよね。

いろいろ調べたところ、django-plotly-dashというパッケージが良さそうということで、試してみました。一部、苦労しましたが、うまくいきましたので、紹介します。

django-plotly-dash — django-plotly-dash documentation

前回Dashにて描いた世界地図をDjangoに統合する例を説明していきます。

環境

  • python==3.7.7
  • Django== 3.0.8
  • dash==1.11.0
  • dash-bootstrap-components==0.10.3
  • django-bootstrap4==2.2.0
    以下は、世界銀行データからグラフを作成するのに使います。
  • pandas==1.0.5
  • world-bank-data==0.1.3

まずはDjangoのプロジェクト・アプリを設定しておく

django-admin startproject djangomap

任意のプロジェクト名(ここではdjangomap)にてDjangoプロジェクトを設定します。 このプロジェクト名のフォルダができるので、そのフォルダ下に移動し、アプリケーションを作成します。 ここではworldmapというアプリケーション名にしています。

python manage.py startapp worldmap

Django-plotly-dashをインストールする

pip install django_plotly_dash

ドキュメンテーションに従って、以下もインストールしておきます。

pip install channels daphne redis django-redis channels-redis

さらに以下もインストールします。

pip install dpd-components dpd-static-support

settings.pyの設定

settings.pyを変更していきます。まずは、インストールしたアプリケーションを利用できるように設定します。

 # djangomap/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_plotly_dash.apps.DjangoPlotlyDashConfig',  # django-plotly-dashにより追加
    'channels', # django_plotly_dashとともにchannelsをインストールしたため追加
    'channels_redis', # django_plotly_dashとともにchannels_redisをインストールしたため追加
    'bootstrap4',   #django-bootstrap4を利用する場合
    'worldmap.apps.WorldmapConfig' # 通常のdjangoアプリ設定に伴う追加
]

続いて、以下を丸々、settings.pyの最後に追加します。

  ### django_plotly_dashにて追加(ここから) ###

ASGI_APPLICATION = 'djangomap.routing.application'  # djangomapのところにはプロジェクト名が入ります。

CHANNEL_LAYERS = {
    'default':{
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts':[('127.0.0.1', 6379),],
        }
    }
}

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'django_plotly_dash.finders.DashAssetFinder',
    'django_plotly_dash.finders.DashComponentFinder'
]

PLOTLY_COMPONENTS = [
    'dash_core_components',
    'dash_html_components',
    'dash_renderer',
    'dpd_components',
    'dpd_static_support',
    'dash_bootstrap_components',
]

X_FRAME_OPTIONS = 'SAMEORIGIN'

 ### django_plotly_dashにて追加(ここまで) ###

urls.pyを修正(プロジェクトレベル)

プレジェクトレベル(ここではdjangomapフォルダ直下)の`urls.py'に以下を追加する。

urlpatterns = [
    path('', include('worldmap.urls')), 
    path('admin/', admin.site.urls),
    path('django_plotly_dash/', include('django_plotly_dash.urls')), # これを追加
]

routing.pyを追加

プロジェクトフォルダ直下にrouting.pyを追加します。 routing.pyには以下を記述します。

from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({

})

Dashアプリの変更

前回作ったDashアプリを修正していきます。アプリケーションレベル(ここではworldmapフォルダ直下)に、dashアプリ - import dashfrom django_plotly_dash import DjangoDashに変更する
- dash.Dash(...)DjangoDash('WorldMap', add_bootstrap_links=True)に変更する
- if name == "main": app.run_server() を削除する

Dashアプリを表示するhtmlファイルを作成

アプリケーションレベル(ここではwouldmap直下)にtemplatesフォルダを作成、さらにworldmapフォルダを作成(Djangoの慣習)。そこに例えば、index.htmlを作成する(名前は任意)。そのファイルには以下の通り、記述する。

{% extends 'base.html' %}

{% block content %}

  {% load plotly_dash %}
  <div class="{% plotly_class name='WorldMap' %} card" style="height: 100%; width: 100%;">
    {% plotly_app name='WorldMap' ratio=1.0 %}
  </div>

{% endblock %}

順番が前後してしまいましたが、プロジェクトと同レベルにtemplatesフォルダを作成し、その中にbase.htmlを作成します。

そのbase.htmlには、ここではサンプルとして、Bootstrap4.3の以下のページの最初に出てくるNavbarをコピペして利用します。

getbootstrap.jp

views.pyとurls.py(アプリケーションレベル)

アプリケーションレベル(ここではworldmap)のviews.pyは通常通り。

 # views.py

from django.shortcuts import render

def index(request):
    return render(request, 'worldmap/index.html')

urls.pyでは、Dashアプリをインポートしておく必要があります。

  # urls.py
from django.urls import path
from . import views
from . import worldmap # これが必要

app_name = 'worldmap'

urlpatterns = [
    path('', views.index, name='index')
]

これで設定できました。さて、migrateを忘れるとうまく動きません。

python manage.py migrate

を忘れずに。そして、以下で起動してみると。

python manage.py rumserver

f:id:akatak:20200725160431p:plain

うまく行きました!

スクリプトはこちらにアップしておきました。

GitHub - tak-akashi/djangomap: Simple Web Application with Django and Dash

番外編

なお、上記の簡単な例をベースに、より機能強化を図ったサイトを作成し、Herokuにデプロイしましたので、こちらにもよろしければどうぞ。

https://egraph.herokuapp.com

可視化フレームワークDashで世界統計地図を描いてみた

Dashとはカナダに拠点をおくPlotly社が開発しているPythonから利用できるWebフレームワークで、様々なデータの可視化・グラフ化に利用できます。

以前紹介したPlotlyというPythonライブラリもこのDashで使えますし、Dashでは独自にグラフ化のためのコンポーネントを用意しているほか、htmlのタグ等もコンポーネント化していますので、簡単なWebページが作成できるというものです。Plotlyと同様にインタラクティブにグラフの操作が可能です。また、pandasのDataFrameを使えるのも良いですよね。

今回は、世界銀行が公表しているデータをworld_data_bankというライブラリを使って取得。このデータをDash上にグラフ化し、Herokuにデプロイしました。画面はこんな感じ。

f:id:akatak:20200711164239p:plain

サイトへのリンクはこちら↓
https://world-bank-map.herokuapp.com

左上のドロップダウンにて「一人当たりGDP(ドル)」「平均寿命」「人口」。隣のドロップダウンで「西暦年」を選択すると、グラフが表示されます。右のドロップダウンはDashで利用可能なカラーパレットです。いろいろなカラースケールを確かめたく、選択できるようにしてみました。また、グラフ表示だけでなく、タブでテーブルを選択するとグラフ表示で利用したデータをテーブル形式で見られるようにもしました。

世界銀行の公表データの取得

世界銀行(World Bank)は数多くのデータを公表していて、APIを利用して取得することができます。Pythonの場合、世界銀行APIを直接利用しなくても、このAPIを利用したモジュールがいくつかあります。

有名どころではpandas_datareaderを利用することができます。 pandas-datareader.readthedocs.io

今回は、公表データの一覧表等を比較的入手しやすいworld_bank_dataというモジュールを利用しました。 pypi.org

インストールは以下の通り。

pip install world_bank_data --upgrade

どんな感じのデータか見てみましょう。world_bank_datapandasをインポートしておきます。

import world_bank_data as wbd
import pandas as pd

データを取得するには何のデータか特定する必要があります。World BankではIndicatorと呼んでいますが、こちらのサイトから検索できます。

Data Catalog | Data Catalog あるいは、world_bank_dataget_indicators()メソッドで全てのIndicatorを取得(17473 行!)して探すか、あるいはsearch_indicators('キーワード')で検索するかして、入手したいデータのindicatorを特定します。

今回は、「一人当たりGDP(ドル)」「平均寿命」「人口」を取得したいと思います。それぞれindicatorはNY.GDP.PCAP.KD, SP.DYN.LE00.IN, SP.POP.TOTLとなります。

get_series('インディケーター')メソッドで取得すると

f:id:akatak:20200711153547p:plain

MultiIndexのDataFrameが返されます。MultiIndexのうち、CountryとYearを軸にこれら3つのDataFrameをpandasのmergeを使って統合します。ついでに、indicatorをわかりやすい日本語に変えておきます。

_df = pd.merge(df_gdp_pcap,df_lifeexp, on=['Country', 'Year'])
_df = pd.merge(_df, df_pops, on=['Country', 'Year'])
_df.columns = ['一人当たりGDP(ドル)', '平均寿命(歳)', '人口(人)']

f:id:akatak:20200711154121p:plain

Dashのコロプレス図(choropleth map)を描くには、3文字の国コードが必要になります。幸いworld_bank_dataでは簡単に国・地域の一覧が入手できます。

df_all_countries = wbd.get_countries()

結果を見てみると、一番左のidが3文字の国コードですね。

f:id:akatak:20200711154642p:plain

これを先ほどの_dfと統合します。_dfのCountry欄とdf_all_countriesのname欄の紐つけています。

df = pd.merge(_df.reset_index(), df_all_countries.reset_index(), left_on='Country', right_on='name')

f:id:akatak:20200711155505p:plain

これだと、Country 欄にはまだ、国以外に地域(東アジア、ヨーロッパ等)が残っていますので、国だけのリストにします。ついでに、不要な欄を削除し、Year欄を文字列から整数列に変えておきます。

df = df[df['region'] != 'Aggregates'].reset_index(drop=True)
df = df.drop(['iso2Code','name', 'adminregion', 'incomeLevel', 'lendingType', 'capitalCity', 'longitude','latitude'], axis=1).reset_index(drop=True)
df['Year'] = df['Year'].astype(int)

Dashアプリを作成する

Dashについて、細かく説明するのは大変なので、省略します。すみません(笑)。詳しくは、本家の英語のTutorialを参照いただくか、日本語でチュートリアルを解説されているサイトをご参照いただければと思います。

dash.plotly.com

qiita.com

ここではチュートリアルに書かれていないポイントを中心にいくつか記載します。

DashでBootstrapを利用する

今回タブを使っています。Dashにおいて標準で提供されているタブはdash_core_componentのTabなんですが、横に間延びしてしまって、格好が悪いんですよね。なので、dashでBootstrapを利用するためのモジュールであるdash_bootstrap_componentsをインストールして利用しています。

Dash Bootstrap Components

このdash_bootstrap_componentsのTabは、すっきりしていて良いのですが、標準では使わなくてよいcallback関数を使わないといけなくなるのが若干のマイナスポイントですかね。

コロプレス図はplotly.graph_objectsを利用する

Plotlyが最近導入したplotly.expressは、グラフを簡単に描くためのモジュールで、Dashでも使えます。ただし、コロプレス図においてcallback関数を使う場合には、ページがリフレッシュされないという問題がPlotly Community Forumにも報告されています。なので、Dashにてコロプレス図を書く場合にはplotly.graph_objects.Choroplethを利用します。

棒グラフ(Bar Chart)にカラーパレットを適用する

go.Barの属性にmarkerを設定します。

 go.Bar(
        x=df_selected['Country'],
        y=df_selected[item],
       marker={
               'color': df['人口'],                  # データ系列をセット
               'colorscale': 'viridis'            # カラーパレットを設定
       }
)

テーブルのフォーマットを変更する

以下の通り、設定します。

dash_table.DataTable(
       # 省略

       # 特定のカラムのセルを左寄せにする
        style_cell_conditional = [
               {
                     'if': {'column_id': c},
                     'textAlign': 'left'
               } for c in ['Country', 'id', 'region']
         ],

  # 1行おきに背景色を変える
        style_data_conditional = [
               {
                    'if': {'row_index': 'odd'},
                    'backgroundColor': 'rgb(248, 248, 248)'
               },
         ],

       # タイトル行の背景色を変える・太字にする
        style_header={
               'backgroundColor': 'rgb(230, 230, 230)',
               'fontWeight': 'bold'
        }
)

なお、特定行の小数点を2桁にしたいと思って色々と試したけど、うまくいきませんでした。ご存知の方がいたら教えていただけると助かります。

スクリプト全体はこちら↓をご参照ください。

github.com

Djangoで読書管理Webアプリを作る

ちょうど一年ほど前に、「FlaskとSQLAlchemyで読書記録Webアプリを作る」という記事を書きました。

その際に「DjangoはWebアプリケーションの作成までの決まりごとが多く、ハードルが高いため、とりあえずでいいからWebアプリケーションを簡単に作ってみたい方にはFlaskがオススメです。」と言っておりました。

確かにそうなのですが、その後がありました。Flaskは"Hello, World!"や簡単なホームページを作成するのはハードルが低いのですが、だんだんと機能を強化していくには、Flask-SQLALchemyやFlask-Migrateだったり、flask-WTFだったり、様々なモジュールを追加していかなくてはならないんですよね。これが結構、面倒だったりします。

一方で、Djangoは最初のハードルは高いのですが、一旦、当初の一連のセッティングや決まりごとを理解すると、なんといっても三大フレームワークの一つ、Webアプリケーションの機能強化を比較的簡単に図っていけることに気が付きました。

特に、デフォルトのAdmin(管理)画面を利用して、ユーザー登録機能が比較的簡単にできるし、デフォルトで十分なパスワードの暗号化が図られていたり、と至れり尽せり、という感じでしょうか。ただ、機能がありすぎて、公式ページで発見したDjango DocumentationというPDFをダウンロードしたところ、何と1996ページ!(笑)。まぁ、そのうち一部でも使えれば良いと考えることにしました。

ということで、昨年Flaskで作成した読書記録Webアプリの機能強化バージョンをDjangoで作成し、Herokuにデプロイしました。

なお、個人別に読書記録を管理する必要がありますので、ユーザー登録(メールアドレス・パスワード)は必須となっています。ただし、最終的にアプリを利用する必要がなくなった場合に、ユーザー登録は削除可能となっており、Heroku上のpostgreSQLからメールアドレスの情報も含めて削除されますので、ご安心ください。

【追記(2020/8/30)】

個人としてユーザー登録せずに機能の確認をしていただけるように、共有とはなりますが、以下のメールアドレス(テスト用)とパスワード(テスト用)を設定しました。是非ご利用ください。

メールアドレス(テスト用): test@user.com
パスワード(テスト用): testpassword

django-book-records.herokuapp.com

インデックスページ、ユーザー登録ページ、読書記録の入力ページのイメージを貼り付けておきます。

f:id:akatak:20200701154254p:plain

f:id:akatak:20200701154606p:plain

f:id:akatak:20200701154624p:plain

 アプリケーションの概要

django-book-records

Django 2.2を利用した読書記録を管理するWebアプリケーションです。Herokuサービスに登録・アップロードしています。

主な機能

主な機能は以下の通りです。

  • ユーザ登録機能。メール・パスワードを入力し仮登録。そして、入力したメールアドレスに自動メールを送信、本文に記載のリンクをクリックことで本登録完了。
  • パスワード変更可能。また、パスワードを忘れた場合への対応機能も実装。
  • ユーザー毎に読書記録の登録機能。入力フィールドは、以下の6つです。
    1. タイトル(必須)
    2. ジャンル
    3. 著者名
    4. おすすめ度(5段階)(デフォルト=「3:普通」)
    5. 読了日(デフォルト=当日)
    6. コメント
  • 入力データの登録、更新、削除に対応しています。
  • ジャンル登録機能。読書記録とは別画面で、読書記録登録前に先に登録しておく必要があります。
  • 著者名登録機能。読書記録とは別画面で、読書記録登録前に先に登録しておく必要があります。
  • その他の機能として、「インポート」「エクスポート」「一括削除機能」があります。
  • インポート機能は、csv形式のファイルからデータを取り込む機能です。「その他」の「インポート」を選択すると、ファイル選択画面に推移します。留意点は以下の通り。
    1. 1行目及び1列目は取り込まない仕様になっています。csvファイルを作成する際には、1行目をタイトル行、1列目を通し番号に利用する等、適宜ご利用ください。
    2. csvの各行は「No」「タイトル」「ジャンル」「著者」「読了日」「おすすめ度」「コメント」の順で、作成してください。
    3. 「読了日」は YYYY-MM-DDのフォーマットで作成してください。
    4. おすすめ度は、1〜5の間で半角数字にて指定してください(5: とてもオススメ, 4:オススメ, 3: 普通, 2: どうだろう, 1: オススメせず)。 一旦、ひとつ読書記録を入力し、エクスポート機能を利用してcsvファイルを作成し、そのファイルを更新すると比較的楽にインポート用のcsvファイルを作成できるかと思います。
  • エクスポートは、「その他」の「エクスポート」をクリックすることで自動的に読書記録が全件ダウンロードされます。
  • その他「一括削除機能」があります。
  • 検索機能については、スペースで区切った複数語検索に対応しています。
  • Bootstrap4を利用しています。入力フォームではdjango-bootstrap4を利用してます。
  • Bootstrap Pagination を利用しています。1ページあたりの表示件数を10件に設定しています。
  • 読了日は、当日から10年前から翌年まで登録可能です。

Requirement

Django==2.2.5
django-bootstrap4==2.0.1

参考にさせていただいたサイト

ユーザー登録機能は以下のサイトに記載のスクリプトを利用させていただいております。

narito.ninja

その他、インポート・エクスポート機能も大いに参考にさせていただきました。ありがとうございました。

narito.ninja

PlotlyとTA-libでテクニカル分析チャートを描く

前回はPythonを利用して株価のローソク足チャートを描くのに、PlotlyとCufflinksを使いました。Cufflinksを使うと、ローソク足チャートもテクニカル分析用のチャートもすごく簡単にきれいに描けました。

それはそれで良いのですが、今後、アルゴリズムトレードなどを研究していくにあたり、テクニカル分析のチャートだけでなく、データを利用できるといいですよね。

いろいろと調べていくと、テクニカル分析にはTA-Libというモジュールが良いようです。早速利用してみましょう。

TA-Libのインストール

これがうまくいかないですよね〜。私はMacを使っているのですが、conda install ta-lib, pip install ta-libもうまくいかない。ネットで調べると、困っている方がそれなりにいて、それぞれ解決策が異なっているようです。私の環境ではうまくいかず、どこかのサイトにPythonのバージョンを下げたらインストールできたという記事が載っていたので、試してみました。

新たにPython 3.5の仮想環境を作ったら、なんと今回はインストールできました。環境は個々に異なりますので、うまくいかない方はPythonのダウングレードを試してみると良いかもしれません。

TA-Libを使ってみる

TA-Lib
こちらのサイトに、簡単な使い方が書かれています。
かなりのテクニカル分析が網羅されているようで、人気があるのもわかる気がします。

さて、必要なライブラリをインポートしておきます。

# TA-Libをインポート
import talib as ta

# 株価を取得するためにpandas_datareaderもインポート
import pandas_datareader.data as web

# numpyとpandasは必須
import numpy as np
import pandas as pd

# チャートを描くのにPlotlyを利用します
import plotly.offline as pyo
import chart_studio.plotly as py
import plotly.graph_objs as go

Jupyter Notebookを利用してPlotlyのグラフを作成するには、以下のコマンドを実行しておきます。

pyo.init_notebook_mode()

さて、前回同様、pandas_datareaderを利用して、Yahooから日経225の株価データを入手します。

df = web.DataReader('^N225', 'yahoo', '2018-01-01')

これで2018/1/1以降、直近までの株価が入手できます。

さて、データの最後の5行を見ておきます。 f:id:akatak:20191123211237p:plain

問題ないですね。もし、最後の1行にデータがない(ゼロ)の場合には、以下の通り、最終行を除いておきます。

df = df.iloc[:-1]

さあ、それでは、テクニカル分析用にデータを作成していきます。TA-Libはnumpyのarray形式でしか入力を受け付けないようですので、高値・安値・終値をDataFrame, Series形式からnumpy.array形式に変換します。始値は使いません。

h = np.array(df['High'])
l = np.array(df['Low'])
c = np.array(df.loc[:, 'Close']) #このようにしても同じです。

単純移動平均 (Simple Moving Average)

sma20 = ta.SMA(c, timeperiod=20)
sma50 = ta.SMA(c, timeperiod=50)
sma200 = ta.SMA(c, timeperiod=200)

# PlotlyではDataFrameを使うので、以下の通り、dfに追加しておきます。
df['SMA20'] = sma20
df['SMA50'] = sma50
df['SMA200'] = sma200

指数平滑化移動平均(Exponential Moving Average)

ema20 = ta.EMA(c, timeperiod=20)
ema50 = ta.EMA(c, timeperiod=50)
ema200 = ta.EMA(c, timeperiod=200)

df['EMA20'] = ema20
df['EMA50'] = ema50
df['EMA200'] = ema200

MACD (Moving Average Convergence/Divergence)¶

macd, macd_sig, macd_hist = ta.MACD(c, fastperiod=12, 
                                        slowperiod=26,
                                        signalperiod=9)

df['MACD'] = macd
df['MACD_SIG'] = macd_sig
df['MACD_HIST'] = macd_hist

RSI (Relative Strength Index)¶

rsi_long = ta.RSI(c, timeperiod=14)
rsi_short = ta.RSI(c, timeperiod=7)

df['RSI_LONG'] = rsi_long
df['RSI_SHORT'] = rsi_short

WILLR (Williams' %R)¶

willr = ta.WILLR(h, l, c, timeperiod=14)

df['WILLR'] = willr

Stochastic (STOCH)

slowk, slowd = ta.STOCH(h, l, c, fastk_period=5, slowk_period=3,
                        slowk_matype=0, slowd_period=3, slowd_matype=0)

df['SLOWK'] = slowk
df['SLOWD'] = slowd

Bolinger Bands (BBANDS)

u_band, m_band, l_band = ta.BBANDS(c, timeperiod=5,
                                   nbdevup=2, nbdevdn=2, matype=0)

df['BBAND_U'] = u_band
df['BBAND_M'] = m_band
df['BBAND_L'] = l_band

ADX, DI+ and DI-

adx = ta.ADX(h,l,c,timeperiod=14)
df['ADX'] = adx

DI_plus = ta.PLUS_DI(h, l, c, timeperiod=14)
DI_minus = ta.MINUS_DI(h, l, c, timeperiod=14)
df['DI_plus'] = DI_plus
df['DI_minus'] = DI_minus

Plotlyでチャート化してみる

さて、テクニカル分析データを作成しましたので、チャート化してみましょう。

Plotlyではdatalayoutをそれぞれ作成してから、グラフ化します。 まずはlayoutから。

layout = {
    'height':1000,
    'title' : {'text':'日経225推移', 'x':0.5},
    'titlefont': {'size':25},
    'xaxis': {'title': "", 'rangeslider':{'visible':False}},
    'yaxis' : {'domain': [.55, 1], 'title': "価格(円)" ,'side':"left", 'tickformat':',' },
    'yaxis2': {'domain': [.45, .55], 'title': "RSI", 'side':"right"},
    'yaxis3': {'domain': [.35, .45], 'title': "MACD", 'side':"right"},
    'yaxis4': {'domain': [.25, .35], 'title': "Will%R", 'side':"right"},
    'yaxis5': {'domain': [.15, .25], 'title': "STOCHASTIC", 'side':"right"},
    'yaxis6': {'domain': [.05, .15], 'title': "ADX&DI", 'side':"right"},
}

heightで全体の高さを指定しておいて、各yaxisdomainにて、全体を1.0としたときの、各チャートのy軸に占める位置を[.05, .15]のように指定します。

まずは、ローソク足チャートを作成します。plotlyにはローソク足用のメソッドが用意されています。

trace = go.Candlestick(
    x     = df.index,
    open  = df['Open'],
    high  = df['High'],
    low   = df['Low'],
    close = df['Close'],
    yaxis = 'y1',
    name  = '日経225'
)

そして、以下の通り、datalayoutを指定します。

fig = {'data':[trace] ,'layout':layout}

ここで一旦チャートを描いてみましょう。

f:id:akatak:20191123215152p:plain

ここにテクニカル分析チャートを追加します。一度に追加してしまいましょう。

fig['data'].extend([
    go.Scatter(yaxis="y1" ,x=df.index ,y=df["EMA20"], name= 'EMA20', 
               line=dict(color='lightblue' ,width=1)),
    
    go.Scatter(yaxis="y1" ,x=df.index ,y=df["EMA50"] ,name= 'EMA50',
               line=dict(color='cyan' ,width=1)),

    go.Scatter(yaxis="y1" ,x=df.index ,y=df["EMA200"] ,name= 'EMA200',
               line=dict(color='darkblue' ,width=1)),
    
    go.Scatter(yaxis="y2" ,x=df.index ,y=df["RSI_LONG"] ,name= 'RSI Long',
               line=dict(color='yellowgreen' ,width=1)),

    go.Scatter(yaxis="y2" ,x=df.index ,y=df["RSI_SHORT"] ,name= 'RSI Short',
               line=dict(color='orange' ,width=1)),
    
    go.Scatter(yaxis="y3" ,x=df.index ,y=df["MACD"] ,name= 'MACD',
               line=dict(color='cornflowerblue' ,width=1)),
    
    go.Scatter(yaxis="y3" ,x=df.index ,y=df["MACD_SIG"] ,name= 'MACD(SIG)',
               line=dict(color='red' ,width=1)),
    
    go.Scatter(yaxis="y4" ,x=df.index ,y=df["WILLR"] ,name= 'WILL%R',
               line=dict(color='blue' ,width=1)),  
    
    go.Scatter(yaxis="y5" ,x=df.index ,y=df["SLOWK"] ,name= 'STOCHASTIC %K',
               line=dict(color='blue' ,width=1)), 

    go.Scatter(yaxis="y5" ,x=df.index ,y=df["SLOWD"] ,name= 'STOCHASTIC %D',
               line=dict(color='red' ,width=1)), 

    go.Scatter(yaxis="y6" ,x=df.index ,y=df["ADX"] ,name= 'ADX',
               line=dict(color='darkgreen' ,width=2)), 
    
    go.Scatter(yaxis="y6" ,x=df.index ,y=df["DI_plus"] ,name= 'D+',
               line=dict(color='orange' ,width=2)), 
    
    go.Scatter(yaxis="y6" ,x=df.index ,y=df["DI_minus"] ,name= 'D-',
               line=dict(color='lightgreen' ,width=2)), 
])

pyo.iplot(fig)

こんな感じで、うまく描けました!Plotlyはカーソルをグラフの上に持っていくと、数値が表示されるし、一部を拡大・縮小したりできるので、便利ですよね。そして、Plotlyに利用者として登録しておくと、上記のように動くグラフをブログに貼り付けたりできますので、おすすめです。

[2020/7/8 追記]

前回のブログにも記載した通り、今回のグラフも、よくみるとx軸には土日祝日もプロットされています。すなわち、プロットがなくギャップがあいているグラフとなっています。これを解消する方法を偶然知りましたので、追加情報として共有いたします。実は、layoutのxaxisに'xaxis': {'type': 'category'}を追加すると、土日祝日がx軸から除外されるようになります。ご確認いただけると幸いです。