utamaro’s blog

誰かの役に立つ情報を発信するブログ

frappe-ganttでグラフを更新する方法

frappe-ganttのドキュメントからチャートの更新apiを探してみたのですが、見つけられませんでした。

issueを見てみたところ、解決策が見つかったので紹介します。

https://github.com/frappe/gantt/issues/44

jsfiddleにコードを公開してくださっているのでリンクを載せます。

https://jsfiddle.net/stvkas/jgtygxro/14/

コーディング

refresh関数を使用すると良いみたいです。

ボタンを押すと、ランダムなタスクが追加されます。

html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Simple Gantt</title>
        <link rel="stylesheet" href="../dist/frappe-gantt.css" />
        <link rel="stylesheet" href="./index.css" />
    </head>
    <body>
        <div class="container">
            <!-- <h2>Interactive Gantt Chart entirely made in SVG!</h2> -->
            <svg id="gantt"></svg>
        </div>
        <button id="add-task">Add Tasks</button>

        <script src="./moment.js"></script>
        <script src="./snap.svg-min.js"></script>
        <script src="./fg.min.js"></script>
        <script src="./index.js"></script>
    </body>
</html>

javascript

function dinamic() {
    var names = [
        ["Redesign website", [0, 7]],
        ["Write new content", [1, 4]],
        ["Apply new styles", [3, 6]],
        ["Review", [7, 7]],
        ["Deploy", [8, 9]],
        ["Go Live!", [10, 10]]
    ];
        
    var tasks = names.map(function(name, i) {
        var today = new Date();
        var start = new Date(today.getFullYear(), today.getMonth(), today.getDate());
        var end = new Date(today.getFullYear(), today.getMonth(), today.getDate());
        start.setDate(today.getDate() + name[1][0]);
        end.setDate(today.getDate() + name[1][1]);
        return {
            start: start,
            end: end,
            name: name[0],
            id: "Task " + i,
            progress: parseInt(Math.random() * 100, 10)
        }
    });
        
    tasks[1].progress = 0;
    tasks[1].dependencies = "Task 0";
    tasks[2].dependencies = "Task 1";
    tasks[3].dependencies = "Task 2";
    tasks[5].dependencies = "Task 4";
    tasks[5].custom_class = "bar-milestone";
    
    var gantt_chart = Gantt('#gantt', tasks);
    
    document.getElementById('add-task').addEventListener('click', function() {
        var task = RandomTask('Task 5');
        tasks.push(task);
        gantt_chart.refresh(tasks);
    });
    
    function RandomTask(deps) {
        var start = new Date();
        start.setDate(start.getDate() + 11 + randomInt(2));
        
        var end = new Date();
        end.setDate(start.getDate() + randomInt(7));
            
        return {
            start: start,
            end: end,
            name: 'Party',
            id: 'Task ' + tasks.length,
            progress: randomInt(100),
            dependencies: deps
        };
    
    }
        
    function randomInt(limit) {
        return Math.floor(Math.random() * limit);
    }
}

frappe-ganttを使ってガントチャートを作成する方法

ガントチャートを作成するライブラリは多くありますが、今回紹介するのはfrappe-ganttというライブラリです。

github.com

↓のようなチャートを作成できます。

f:id:miyaji-y26:20181004191444p:plain

残念ながら、マウス操作でタスクを追加することはできません。自分で実装することはできそうなので挑戦しても良いかもしれません。

使用方法

まずはソースコードをダウンロードします。

https://github.com/frappe/gantt/archive/master.zip

使用するファイルは以下のファイルです。(css以外使いません。)

  • dist/frappe-gantt.css

次に、デモページを開きます。

そこから、frappe-gantt.min.jsをコピーしてきます。

2018/10/04からファイルに変更がなければ↓から取得できるはずです。

(https://frappe.io/gantt/js/frappe-gantt.min.js)

コピーする理由はv0.3.0に不具合があるためです。眼の前の動いているものを使ったほうが安全です。

frappe-ganttはmoment.jssnap-svgに依存性を持っているので、両方ダウンロードします。

http://momentjs.com/

http://snapsvg.io/

コーディング

↓のディレクトリ構造を作成します。

sample
├── frappe-gantt.min.js
├── index.css  ⇠ 自分で作成する
├── index.html  ⇠ 自分で作成する
├── index.js  ⇠ 自分で作成する
├── moment.js
└── snap.svg-min.js

あとはコードを書くだけです。

index.html

#ganttがチャート追加の対象です。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Simple Gantt</title>
        <link rel="stylesheet" href="../dist/frappe-gantt.css" />
        <link rel="stylesheet" href="./index.css" />
    </head>
    <body>
        <div class="container">
            <svg id="gantt"></svg>
        </div>
        <script src="./moment.js"></script>
        <script src="./snap.svg-min.js"></script>
        <script src="./fg.min.js"></script>
        <script src="./index.js"></script>
    </body>
</html>

index.js

サンプルのタスクを画面に追加して、クリックイベントをつけています。

(function() {
    var tasks = [
        {
            start: '2018-10-01',
            end: '2018-10-08',
            name: 'Redesign website',
            id: "Task 0",
            progress: 20
        },
        {
            start: '2018-10-03',
            end: '2018-10-06',
            name: 'Write new content',
            id: "Task 1",
            progress: 5,
            dependencies: 'Task 0'
        },
        {
            start: '2018-10-04',
            end: '2018-10-08',
            name: 'Apply new styles',
            id: "Task 2",
            progress: 10,
            dependencies: 'Task 1'
        },
    ]
    function init() {
        var gantt_chart = new Gantt("#gantt", tasks, {
            on_click: function (task) {
                console.log(task);
            },
            on_date_change: function(task, start, end) {
                console.log(task, start, end);
            },
            on_progress_change: function(task, progress) {
                console.log(task, progress);
            },
            on_view_change: function(mode) {
                console.log(mode);
            },
            view_mode: 'Month',
            language: 'en'
        });
        console.log(gantt_chart);
    }

    document.addEventListener("DOMContentLoaded", function() {
        console.log("start application.");
        init();
    }, false);
})()

index.css

デモと同じような見た目で表示されるようにします。

body {
    font-family: sans-serif;
    background: #eee;
}
.container {
    width: 80%;
    margin: 0 auto;
    overflow: auto;
    border: 1px solid #d8d8d8;
    background-color: #fff;
}

/* custom frappe-gant class */
.gantt .bar-progress {
    fill: tomato !important;
    /* ↑ 進行度のバーがトマト色になります */
}

一番下のタスクと横スクロールの距離が離れすぎている問題

最新版(v0.3.0)を使ってみたところ、一番下のタスクと横スクロールの距離が離れすぎている問題がありました。

これはissueにも載っていたのでもしかしたら対応されるかもしれません。

https://github.com/frappe/gantt/issues/91

解決方法としては、この問題が起きないバージョンを使用することです。

私の場合は、デモページでサンプルを検証ツールで確認して、使用されているライブラリをそのままコピーしました。

https://frappe.io/gantt

(問題が起きないバージョンをcheckoutでたどるより早いと判断しました。)

postgresqlをmacで使う

環境設定

brew install postgresql

起動

postgres -D /usr/local/var/postgres
2018-09-22 05:36:28.073 JST [41369] LOG:  listening on IPv6 address "::1", port 5432
2018-09-22 05:36:28.073 JST [41369] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2018-09-22 05:36:28.075 JST [41369] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2018-09-22 05:36:28.099 JST [41370] LOG:  database system was shut down at 2018-09-22 05:34:20 JST
2018-09-22 05:36:28.108 JST [41369] LOG:  database system is ready to accept connections

ポートを指定したい場合

postgres -D /usr/local/var/postgres -p 54321

別のコンソールを開いて、DBにつなげる

psql -d postgres
psql (10.5)
Type "help" for help.

postgres=#
postgres=# help
You are using psql, the command-line interface to PostgreSQL.
Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit
postgres=#

いろんな設定値を確認したい場合

postgres=# show all

表示を縦にしたいとき(もとに戻したいとき)

\x

テーブル一覧を確認したい場合。(事前に\connectが必要) <table_name>を入れなければすべて、入れると、そのテーブルの構造を表示する。

\d <table_name>

設定

create database XXX;

DBに接続するためのコマンド

Connection
  \c[onnect] {[DBNAME|- USER|- HOST|- PORT|-] | conninfo}
                         connect to new database (currently "postgres")
  \conninfo              display information about current connection
  \encoding [ENCODING]   show or set client encoding
  \password [USERNAME]   securely change the password for a user
\connect XXX
postgres=# \connect XXX;
You are now connected to database "XXX" as user "ユーザ名".

ユーザ名は現在のユーザ名がデフォルトになっている。 ユーザ名を指定する場合は、connectコマンドのUSERに値を設定する。

ユーザ追加をする場合

CREATE USER UUU WITH PASSWORD 'pass';

frappe chartを使ってグラフを更新する方法

frappe chartを使ってグラフを更新する方法と、ドキュメントに載っているサンプルを書いています。

最終的に↓ができます。

f:id:miyaji-y26:20181002071905g:plain

html、javascriptをコピペするといくつかのサンプルも確認できます。

導入

まずはインストールします。

npm install frappe-charts

もしくは↓をhtmlにいれます。

動作を確認したい場合はわざわざビルドまでやるのは面倒なので、cdnを利用することが多いです。

<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.1.0/dist/frappe-charts.min.iife.js"></script>

github.com

サンプル

chart_01 ~ 5まではドキュメントに載っているサンプルを試しています。

コピペして試してみてください。

index.html

htmlは<html><head>など一部省略しています。

<body>
    <div id="chart_01"></div>
    <div id="chart_02"></div>
    <div id="chart_03"></div>
    <div id="chart_04"></div>
    <div id="chart_05"></div>
    <!-- ↓ 1秒ごとにグラフを更新する -->
    <div id="chart_06"></div>
</body>

index.js

(function() {

    function frappe_01() {
        /**
         * quick start
         */
        const data = {
            labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
            ],
            datasets: [
                {
                    name: "Some Data", type: "bar",
                    values: [25, 40, 30, 35, 8, 52, 17, -4]
                },
                {
                    name: "Another Set", type: "line",
                    values: [25, 50, -10, 15, 18, 32, 27, 14]
                }
            ]
        }

        const chart = new frappe.Chart("#chart_01", {  // or a DOM element,
                                                    // new Chart() in case of ES6 module with above usage
            title: "My Awesome Chart",
            data: data,
            type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
            height: 250,
            colors: ['#7cd6fd', '#743ee2']
        })
    }

    function frappe_02() {
        /**
         * Axis Charts: Axis chart: What Is It
         */
        data = {
            labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            datasets: [
                { values: [18, 40, 30, 35, 8, 52, 17, -4] }
            ]
        }

        new frappe.Chart( "#chart_02", {
            data: data,
            type: 'line',  //bar or line
            height: 250,
            colors: ['red']
        });
    }

    function frappe_03() {
        /**
         * Axis Charts: Adding more datasets
         */
        var data = {
            labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            datasets: [
                { name: "Dataset 1", values: [18, 40, 30, 35, 8, 52, 17, -4] },
                { name: "Dataset 2", values: [30, 50, -10, 15, 18, 32, 27, 14] }
            ]
        }

        new frappe.Chart( "#chart_03", {
            data: data,
            type: 'line',  //bar or line
            height: 250,
            colors: ['red']
        });
    }

    function frappe_04() {
        /**
         * Axis Charts: Responsiveness
         */
        var data = {
            labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            datasets: [
                { name: "Dataset 1", values: [18, 40, 30, 35, 8, 52, 17, -4] },
                { name: "Dataset 2", values: [30, 50, -10, 15, 18, 32, 27, 14] }
            ]
        }

        new frappe.Chart( "#chart_04", {
            data: data,
            type: 'bar',  //bar
            height: 250,
            colors: ['red'],
            barOptions: {
                spaceRatio: 0.8 // default: 1
            },
        });
    }

    function frappe_05() {
        /**
         * Axis Charts: More Tweaks
         */
        var data = {
            labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            datasets: [
                { name: "Dataset 1", values: [18, 40, 30, 35, 8, 52, 17, -4] },
            ]
        }

        new frappe.Chart( "#chart_05", {
            data: data,
            type: 'bar',  //bar
            height: 250,
            colors: ['red'],
            axisOptions: {
                xAxisMode: 'tick' // default: 'span'
                // ↑ 縦軸がなくなる
            },
        });
    }

    function frappe_06() {
        /**
         * original: data append
         */
        var data = {
            labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            datasets: [
                { 
                    name: "Dataset 1", 
                    values: [18, 40, 30, 35, 8, 52, 17, -4] },
            ]
        }

        var chart = new frappe.Chart( "#chart_06", {
            data: data,
            type: 'bar',  //bar
            height: 250,
            colors: ['red'],
            barOptions: {
                spaceRatio: 0.8 // default: 1
            },
        });

        var data_push = function(){
            var values_size = data.datasets[0].values.length;
            for (var vi = 0; vi < values_size; vi++) {
                data.datasets[0].values.splice(
                    vi, 1, Math.random() * (100 - 0) + 0
                )
            }
            chart.update(data);
            var id = setTimeout(data_push, 1000);
        }
        data_push();
    }

    function init() {
        console.log('called init function.');
        frappe_01();
        frappe_02();
        frappe_03();
        frappe_04();
        frappe_05();
        frappe_06();
    }
    document.addEventListener('DOMContentLoaded', function() {
        init();
    }, false);
})()

可変グリッドレイアウトをMuuriで実装する

画面サイズを変えると、中のグリッドがグリグリと動く画面を作ってみます。

github.com

npmでインストールします。

npm install muuri

注意事項

ドラッグして要素を動かす場合は以下のライブラリをインストールする必要があります。

npm install hammerjs

npmを使わずに実装する場合は↓を全て入れると良いです。

(ドラッグを使わない場合はhammerjsを抜いて良いです。

<script src="https://unpkg.com/web-animations-js@2.3.1/web-animations.min.js"></script>
<script src="https://unpkg.com/hammerjs@2.0.8/hammer.min.js"></script>
<script src="https://unpkg.com/muuri@0.7.1/dist/muuri.min.js"></script>

実装例

この実装例では、scssやnpmを使っているため別途ビルドが必要です。

cssや、import箇所を修正することでビルドが必要なくなります。

index.html

<div class="main">
    <div class="grid">
        <div class="item">
            <div class="item-content">
                <div class="ss">hoge1</div>
            </div>
        </div>
        <div class="item">
            <div class="item-content">
                <div class="ss">hoge2</div>
            </div>
        </div>
        <div class="item">
            <div class="item-content">
                <div class="ss">hoge3</div>
            </div>
        </div>
        <div class="item">
            <div class="item-content">
                <div class="ss">hoge4</div>
            </div>
        </div>
    </div>
</div>

script.js

jqueryを使っていた場合$(document).readyではうまく動作しないことがありました。

ページ更新時に画像要素が重なって表示されました。

おそらくreadyがDom構築が完了した段階で処理が実行されるためだと思います。

画像のサイズまで考慮されずに重なってしまったのかと。

loadにすると、画像の読み込みなどが完了してから実行されるので改善されました。

import $ from 'jquery';
import Muuri from 'muuri';

(function() {

    function init() {
        var grid = new Muuri('.grid');
    }

    $(window).on('load', function() {
        console.log("called ready function");
        init();
    })
})();

style.scss

.main {
    padding-top: 56px;
    height: 100%;
    box-sizing: border-box;
}

.grid {
    position: relative;

    .item {
        position: absolute;
        z-index: 1;
        background: #fff;
        width: 100px;
        height: 100px;
        margin: 3px;
        border: 1px solid #d8d8d8;
        box-sizing: border-box;
    }
    .item.muuri-item-dragging {
        z-index: 3;
    }
    .item.muuri-item-releasing {
        z-index: 2;
    }
    .item.muuri-item-hidden {
        z-index: 0;
    }
    .item-content {
        position: relative;
        width: 100%;
    }
}

エラー対応

Uncaught TypeError: url.indexOf is not a function

jqueryでloadを使おうとした際に置きました。

$(window).load(function() { ... });

ではなく、↓が正しいです。

$(window).on('load', function() { ... });

DjangoでGoogleアカウントの認証機能を入れる方法

Djangoのログイン機能でGoogleのアカウントを使った方法を入れます。

必要なライブラリをインストールします。

pip install django python3-openid social-auth-app-django

(ついでにherokuで必要なライブラリも入れたい場合は↓をどうぞ)

pip install django djangorestframework psycopg2 psycopg2-binary whitenoise dj-database-url gunicorn python3-openid social-auth-app-django

urls.py

from django.contrib import admin
from django.contrib.auth.views import LogoutView, LoginView
from django.urls import path, include
from django.views.generic import TemplateView

urlpatterns = [
    path('admin/', admin.site.urls),
    # ↓ ログイン用の画面
    path('login/', LoginView.as_view(template_name='top.html'), name='login'),
    # ↓ ログアウトしたあとの画面
    path('logout/', LogoutView.as_view(), name='logout'),
    # トップ画面
    path('', TemplateView.as_view(template_name='top.html'), name='home'),
    # ↓ socialログインに必要なpath
    path('auth/', include('social_django.urls', namespace='social')),
]

次にtemplatesディレクトリに以下のようにファイルを作成します。

Project/templates
├── registration
│   └── logged_out.html
└── top.html

registration以下にlogged_out.htmlを作成している理由について。

Logoutviewのtemplate_nameにそのように指定されているからです。(デフォルト)

もちろん、.as_view(template_name='logout.html')のように書き換えても良いです。

class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
    """
    Log out the user and display the 'You are logged out' message.
    """
    next_page = None
    redirect_field_name = REDIRECT_FIELD_NAME
    template_name = 'registration/logged_out.html'
    extra_context = None

settings.py

以下の設定を追加します。

LOGIN_URL = 'login'  # urls.pyで設定したnameに一致するように
LOGIN_REDIRECT_URL = 'home'  # urls.pyで設定したnameに一致するように

# ↓ はgoogleから取得すること。
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'xxxxxxxxxxxx.apps.googleusercontent.com'  #Paste CLient Key
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'xxxxxxxxxxxx' #Paste Secret Key

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,  'templates')],  # ← を追加
        # ry...
    },
]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'social_django',  # ← を追加
]

AUTHENTICATION_BACKENDS = (
    # 'social_core.backends.open_id.OpenIdAuth',  # for Google authentication
    # 'social_core.backends.google.GoogleOpenId',  # for Google authentication
    'social_core.backends.google.GoogleOAuth2',  # for Google authentication
    # 'social_core.backends.github.GithubOAuth2',  # for Github authentication
    # 'social_core.backends.facebook.FacebookOAuth2',  # for Facebook authentication

    'django.contrib.auth.backends.ModelBackend',
)

html

top.html

top画面では、ログインするためのボタンと、ログアウトするためのボタンを用意します。

それぞれのボタンは、ユーザがログインしているかどうかで表示されるかが決まります。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Sample</title>
        <meta name="description" content="google login project">

        {% load static %}
    </head>
    <body class="">
        {% if user.is_authenticated %}
            <a href="{% url 'logout' %}">
                <span>ログアウト</span>
            </a>
        {% else %}
            <a href="{% url 'social:begin' 'google-oauth2' %}">
                <span>ログイン</span>
            </a>
        {% endif %}
    </body>
</html>

logged_out.html

めんどくさいので簡単に作りました。

ログアウト実行後の画面です。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Logout</title>
    </head>
    <body>
        <p class="display-4">You logged out.</p>
        <a class="lead" href="{% url 'home' %}">Home</a>
    </body>
</html>

備考

わざわざlogged_out.htmlを作らなくても、以下の方法でも良いです。

settingsファイルにLOGOUT_REDIRECT_URLを設定しても良いです。

LOGOUT_REDIRECT_URL = 'home

あるいは、urls.pyでnext_pageを指定しても良いです。

path('logout/', LogoutView.as_view(next_page='home'), name='logout'),

pythonでBeautifulSoupを使ったスクレイピング

pythonでBeautifulSoupを使ったスクレイピング

python3.6.4を使用しています。

必要なライブラリをインストールします。

pip install beautifulsoup4 requests lxml

beautifulsoup4スクレイピングのライブラリ。

requestsはgetとかpostのリクエストに使うライブラリ。

lxmlはhtmlをパースするためのライブラリ。

Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation

実装例

検索するとヒットするのはurllibを使ってhtmlを取得するやり方だが、requestsも使えます。

大抵は別のAPIを実行したりするため、requestsで統一したほうが良いと考えています。

selectのやり方はドキュメントを読んで試すこと。listで帰ってきます。

jsで言うところのdocument.querySelectorAllと同じです。一件取得したい場合はselect_oneを使います。

from bs4 import BeautifulSoup
import requests

def execute():
    html_text = requests.get(amedas)
    # statusが200なら...などの条件を入れるのが一般的
    soup = BeautifulSoup(html_text.text, "lxml")
    tbl_prefecture_tag = soup.select(selector="#tbl")[0]
    table_cols = tbl_prefecture_tag.select(selector="th['headers'='COL1']")
    # ... ry

if __name__ == "__main__":
    execute()