20201025のJavaScriptに関する記事は30件です。

階層型クラスタリングをJavaScriptで実装した

はじめに

色々な機械学習処理をブラウザ上で試せるサイトを作った」中で実装したモデルの解説の三回目です。

今回は階層型クラスタリングの実装について解説します。

デモはこちらから。(TaskをClusteringにして、Modelのhierarchyを選択)
実際のコードはhierarchy.jsにあります。

なお、可視化部分については一切触れません。

概説

階層型クラスタリングのうち、以下の7手法を実装します。

  • Complete Linkage(完全連結法)
  • Single Linkage(単純連結法)
  • Group Average(群平均法/UPGMA)
  • Ward's(ウォード法)
  • Centroid(重心法/UPGMC)
  • Weighted Average(重み付き平均法/WPGMA)
  • Median(メジアン法/WPGMC)

実装はこちらの資料を参考にしました。

手法ごとで処理が異なる部分は、クラスタ間の距離の計算とその更新式です。
k-means法ではDependency Injectionの雰囲気で実装しましたが、こちらはクラスを継承して実装しました。

処理の流れ

階層構造の解析

fit関数で木構造を使って階層構造を一気に解析します。

処理の流れは次の通りです。

  1. 木構造のルート直下に、葉としてデータが一つのみ存在するクラスタを設定する。また同時に、各データ間の距離を計算する
  2. ルート直下の全てのノード間の距離を計算する
  3. ルート直下のノードが一つの場合は、処理を終了する
  4. ルート直下のノードの中から、最も距離の近い二つのノードを選択する。この二つのノードをまとめたものが、新しいクラスタとなる。
  5. ルート直下から選択された二つのノードを削除し、その二つを子に持つ部分木をルート直下に入れる
  6. ルート直下の全てのノード間の距離を再計算し、3.に戻る

データ間の距離はユークリッド距離、マンハッタン距離、チェビシェフ距離の三つを使用できるようにしています。

ノード間の距離をdistancesとして二次元配列で保持し、再計算時はノードの削除・挿入と位置が合うように縮めていきます。

また、各ノードには子となる二つのクラスタ間距離を保持しておきます。

Lance-Williamsの更新式

ノード間の距離の再計算は部分的な情報のみから計算することができ、計算量は$O(1)$になります。

結合前のクラスタを$C_a, C_b$、結合後のクラスタを$C_c$とし、それらとは別のクラスタを$C_k$とします。
また、クラスタ$C_x, C_y$間の距離を$d_{xy}$、クラスタ$C_x$に含まれるデータ数を$N_{C_x}$とします。

結合前のクラスタ間の距離$d_{ka}, d_{kb}$が分かっていたとき、結合後のクラスタ間の距離$d_{kc}$は、こちらの資料によると

d_{kc} = \alpha_a d_{ka} + \alpha_b d_{kb} + \beta d_{ab} + \gamma \left| d_{ka} - d_{kb} \right|

となります。ただし式中の$\alpha_a, \alpha_b, \beta, \gamma$は、手法別に次の通りとなります。

$\alpha_a$ $\alpha_b$ $\beta$ $\gamma$
Complete Linkage $\frac{1}{2}$ $\frac{1}{2}$ $0$ $\frac{1}{2}$
Single Linkage $\frac{1}{2}$ $\frac{1}{2}$ $0$ $-\frac{1}{2}$
Group Average $\frac{N_{C_a}}{N_{C_c}}$ $\frac{N_{C_b}}{N_{C_c}}$ $0$ $0$
Ward's $\frac{N_{C_k} + N_{C_a}}{N_{C_k} + N_{C_c}}$ $\frac{N_{C_k} + N_{C_b}}{N_{C_k} + N_{C_c}}$ $-\frac{N_{C_k}}{N_{C_k} + N_{C_c}}$ $0$
Centroid $\frac{N_{C_a}}{N_{C_c}}$ $\frac{N_{C_b}}{N_{C_c}}$ $-\frac{N_{C_a}N_{C_b}}{N_{C_c}^2}$ $0$
Weighted Average $\frac{1}{2}$ $\frac{1}{2}$ $0$ $0$
Median $\frac{1}{2}$ $\frac{1}{2}$ $-\frac{1}{4}$ $0$

クラスタの取得

上記の流れで得られた木を使用すると、クラスタの取得はシンプルになります。
木の中のクラスタ間距離が大きいノードで分割していき、指定されたクラスタ数になるまで続けるだけです。

クラスタ間距離は親ノードよりも子ノードの方が小さいことが保証されますので、幅優先探索の要領でルートから順番にたどっていけば、不要な探索が無くなります。

処理の流れは次の通りです。

  1. クラスタ一覧にすべてのデータが含まれたノードを設定する
  2. クラスタ一覧に含まれるノード数が指定されたクラスタ数になった場合は、そのクラスタ一覧を返して処理を終了する
  3. クラスタ一覧の中から、分割したのちのクラスタ間距離が一番大きいノードを選択する
  4. 選択したノードをクラスタ一覧から取り除き、その子ノードを取り出してクラスタ一覧に追加する
  5. 2.に戻る

なお木の配列を返しているのは、可視化時に使用するためです。
実際のデータは葉ノードにありますので、その部分を走査すれば取得できます。

木構造は、手作り数学ライブラリmath.jsに定義したTreeを使用します。

ここで使用している処理は次の通りです。

  • プロパティ
    • length : 子ノードの数
    • value : ノードが持っている値
  • 関数
    • at : 指定された位置の子ノードを返す
    • push : 子ノードの最後に渡された値を持った木を設定する。ただし、木が渡された場合はそのまま設定する
    • set : 指定した位置に渡された値を持った木を設定する。ただし、木が渡された場合はそのまま設定する
    • removeAt : 指定された位置の子ノードを削除する
    • leafCount : 葉の数を返す
    • isLeaf : 葉ノードの場合はtrueを返す
    • leafValues : 葉ノードの値を配列として返す

コード

親クラス

子クラスで実装する関数はdistanceupdateで、それぞれクラスタ間距離の計算とクラスタ間距離の更新計算を行います。

また、_lanceWilliamsUpdaterによりLance-Williamsの更新式を計算する関数を返します。

class HierarchyClustering {
    constructor(metric = 'euclid') {
        this._root = null;
        this._metric = metric;

        switch (this._metric) {
        case 'euclid':
            this._d = (a, b) => Math.sqrt(a.reduce((s, v, i) => s + (v - b[i]) ** 2, 0));
            break
        case 'manhattan':
            this._d = (a, b) => a.reduce((s, v, i) => s + Math.abs(v - b[i]), 0)
            break
        case 'chebyshev':
            this._d = (a, b) => a.reduce((s, v, i) => Math.max(s, Math.abs(v - b[i])), -Infinity)
            break;
        }
    }

    fit(points) {
        this._root = new Tree()
        points.forEach((v, i) => {
            this._root.push({
                point: v,
                index: i,
                distances: points.map(p => this._d(v, p))
            });
        });

        const distances = []
        for (let i = 0; i < this._root.length; i++) {
            if (!distances[i]) distances[i] = [];
            for (let j = 0; j < i; j++) {
                if (!distances[i][j]) distances[i][j] = distances[j][i] = this.distance(this._root.at(i), this._root.at(j));
            }
        }
        while (this._root.length > 1) {
            let n = this._root.length;

            let min_i = 0;
            let min_j = 1;
            let min_d = distances[0][1];
            for (let i = 1; i < n; i++) {
                distances[i].forEach((d, j) => {
                    if (d < min_d) {
                        min_i = i;
                        min_j = j;
                        min_d = d;
                    }
                });
            }
            let min_i_leafs = this._root.at(min_i).leafCount();
            let min_j_leafs = this._root.at(min_j).leafCount();
            distances.forEach((dr, k) => {
                if (k != min_j && k != min_i) {
                    dr[min_i] = this.update(min_i_leafs, min_j_leafs, this._root.at(k).leafCount(), dr[min_i], dr[min_j], distances[min_j][min_i]);
                    distances[min_i][k] = dr[min_i];
                    dr.splice(min_j, 1);
                }
            });
            distances[min_i].splice(min_j, 1);
            distances.splice(min_j, 1);
            this._root.set(min_i, new Tree({
                distance: min_d,
            }, [this._root.at(min_i), this._root.at(min_j)]));
            this._root.removeAt(min_j);
        }
        this._root = this._root.at(0);
    }

    getClusters(number) {
        const scanNodes = [this._root]
        while (scanNodes.length < number) {
            let max_distance = 0;
            let max_distance_idx = -1;
            for (let i = 0; i < scanNodes.length; i++) {
                const node = scanNodes[i];
                if (!node.isLeaf() && node.value.distance > max_distance) {
                    max_distance_idx = i;
                    max_distance = node.value.distance
                }
            }
            if (max_distance_idx === -1) {
                break
            }
            const max_distance_node = scanNodes[max_distance_idx];
            scanNodes.splice(max_distance_idx, 1, max_distance_node.at(0), max_distance_node.at(1))
        }
        return scanNodes;
    }

    distance(c1, c2) {
        throw new Error('Not Implemented');
    }

    _mean(d) {
        const m = Array(d[0].length).fill(0);
        for (let i = 0; i < d.length; i++) {
            for (let k = 0; k < d[i].length; k++) {
                m[k] += d[i][k]
            }
        }
        return m.map(v => v / d.length);
    }

    _lanceWilliamsUpdater(ala, alb, bt, gm) {
        return (ka, kb, ab) => ala * ka + alb * kb + bt * ab + gm * Math.abs(ka - kb);
    }

    update(ca, cb, ck, ka, kb, ab) {
        throw new Error('Not Implemented');
    }
}

Complete Linkage(完全連結法)

クラスタ間距離は、それぞれのクラスタに含まれるデータの中で一番遠いデータ同士の距離になります。

class CompleteLinkageHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let f1 = c1.leafValues();
        let f2 = c2.leafValues();
        return Math.max.apply(null, f1.map(v1 => {
            return Math.max.apply(null, f2.map(v2 => v1.distances[v2.index]));
        }));
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(0.5, 0.5, 0, 0.5)(ka, kb, ab)
    }
}

Single Linkage(単純連結法)

クラスタ間距離は、それぞれのクラスタに含まれるデータの中で一番近いデータ同士の距離になります。

class SingleLinkageHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let f1 = c1.leafValues();
        let f2 = c2.leafValues();
        let minDistance = Math.min.apply(null, f1.map(v1 => {
            return Math.min.apply(null, f2.map(v2 => v1.distances[v2.index]));
        }));
        return minDistance;
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(0.5, 0.5, 0, -0.5)(ka, kb, ab)
    }
}

Group Average(群平均法/UPGMA)

クラスタ間距離は、それぞれのクラスタに含まれるデータの間の距離の平均になります。

class GroupAverageHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let f1 = c1.leafValues();
        let f2 = c2.leafValues();
        let totalDistance = f1.reduce((acc1, v1) => {
            return acc1 + f2.reduce((acc2, v2) => acc2 + v1.distances[v2.index], 0);
        }, 0);
        return totalDistance / (f1.length * f2.length);
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(ca / (ca + cb), cb / (ca + cb), 0, 0)(ka, kb, ab);
    }
}

Ward's(ウォード法)

クラスタ間距離は、結合後のクラスタの重心との距離の平均から、結合前それぞれのクラスタの重心との距離の平均を引いた値になります。

class WardsHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let f1 = c1.leafValues().map(f => f.point);
        let f2 = c2.leafValues().map(f => f.point);
        let fs = f1.concat(f2);
        let ave1 = this._mean(f1);
        let ave2 = this._mean(f2);
        let aves = this._mean(fs);
        let e1 = f1.reduce((acc, f) => acc + this._d(f, ave1) ** 2, 0);
        let e2 = f2.reduce((acc, f) => acc + this._d(f, ave2) ** 2, 0);
        let es = fs.reduce((acc, f) => acc + this._d(f, aves) ** 2, 0);
        return es - e1 - e2;
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater((ck + ca) / (ck + ca + cb), (ck + cb) / (ck + ca + cb), -ck / (ck + ca + cb), 0)(ka, kb, ab);
    }
}

Centroid(重心法/UPGMC)

クラスタ間距離は、それぞれのクラスタの重心の距離になります。

class CentroidHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let f1 = c1.leafValues().map(f => f.point);
        let f2 = c2.leafValues().map(f => f.point);
        let d = this._d(this._mean(f1), this._mean(f2));
        return d * d;
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(ca / (ca + cb), cb / (ca + cb), -ca * cb / ((ca + cb) * (ca + cb)), 0)(ka, kb, ab);
    }
}

Weighted Average(重み付き平均法/WPGMA)

クラスタ間距離は、二つのクラスタ間の距離の平均を再帰的に計算していくことにより求めます。

class WeightedAverageHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let calcDistRec = function calcDistRec(h1, h2) {
            if (h1.leafCount() == 1 && h2.leafCount() == 1) {
                return h1.value.distances[h2.value.index];
            } else if (h2.leafCount() == 1) {
                return (calcDistRec(h2, h1.at(0)) + calcDistRec(h2, h1.at(1))) / 2;
            } else {
                return (calcDistRec(h1, h2.at(0)) + calcDistRec(h1, h2.at(1))) / 2;
            }
        }
        return calcDistRec(c1, c2);
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(0.5, 0.5, 0, 0)(ka, kb, ab);
    }
}

Median(メジアン法/WPGMC)

クラスタ間距離は、それぞれのクラスタの重心の中間点と、一方の重心との距離になります。

class MedianHierarchyClustering extends HierarchyClustering {
    distance(c1, c2) {
        let m1 = this._mean(c1.leafValues().map(f => f.point));
        let m2 = this._mean(c2.leafValues().map(f => f.point));
        let m = m1.map((v, i) => (v + m2[i]) / 2);
        return this._d(m, m2) ** 2;
    }

    update(ca, cb, ck, ka, kb, ab) {
        return this._lanceWilliamsUpdater(0.5, 0.5, -0.25, 0)(ka, kb, ab);
    }
}

さいごに

文末にセミコロンが付いたり付かなかったり、constでいいところをletを使用していたりと、気になる部分が多い。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】スクロールの途中からヘッダーを出す方法

どうも7noteです。スクロールしたら出てくるヘッダー作ります。

最初は大きく写真を見せて、スクロールした後にヘッダーを表示させたい時の動きです。

※jQueryを使っています。jQeryってなんだ?って方はこちら

ソース

index.html
<!-- html、長いですがほとんど意味のあまりないソースです。 -->
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<header>
  <p>ここがヘッダーです。</p>
</header>
<div class="mv">
  <img src="sample.jpg" alt="メインビジュアル">
</div>

<main>
  <div class="step1">
    <h2>ステップ1</h2>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。</p>
  </div>
  <div class="step2">
    <h2>ステップ2</h2>
    <p>コンバンワコンバンワコンバンワコンバンワ</p>
  </div>

  <!-- ここから先はスクロールさせるための尺稼ぎ -->
  <div class="stepx">
    <h2>ステップx</h2>
    <p>〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜</p>
  </div>
  <div class="stepx">
    <h2>ステップx</h2>
    <p>〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜</p>
  </div>
  <div class="stepx">
    <h2>ステップx</h2>
    <p>〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜</p>
  </div>
  <div class="stepx">
  <h2>ステップx</h2>
    <p>〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜<br>
    〜〜〜〜〜</p>
  </div>
</main>
style.css
header {
  width: 100%;            /* 要素を幅いっぱいにする */
  background: #555;       /* 背景色を濃いグレーに指定。半透明の「rgba(0,0,0,0.5)」でもいいかも */
  display: none;          /* ページ読み込み時は非表示にする */
  position: fixed;        /* ヘッダーを絶対位置にする。スクロールしても固定 */
  top: 0;                 /* 上から0pxに指定 */
  left: 0;                /* 左から0pxに指定 */
  padding: 10px 20px;     /* ちょっと余白をとっとくと綺麗 */
  box-sizing: border-box; /* paddingを含んで幅100%ちょうどにするため */
}
header p {
  color: #fff;            /* 文字色を白にする */
}

.mv {
  width: 100vw;           /* ウィンドウサイズ横幅いっぱいに表示 */
  height: 100vh;          /* ウィンドウサイズ縦幅いっぱいに表示 */
}

.mv img {
  width: 100%;            /* 幅いっぱい */
  height: 100%;           /* 高さいっぱい */
  object-fit: cover;      /* background-size: cover;みたいに全面に収まるように画像を表示(IEでは効かないよ。) */
}
script.js
$(window).scroll(function () {           /* スクロールされた時 */
  var pos = $('main').offset();          /* mvを過ぎたmainタグの高さを取得して変数[pos]に格納 */
  if ($(this).scrollTop() > pos.top) {   /* 変数[pos]より、スクロールされていたら */
    $('header').fadeIn();                /* ヘッダーをふわっと表示 */
  } else {                               /* それ以外の場合 */
    $('header').fadeOut();               /* ヘッダーをふわっと非表示 */
  }
});

出る時はフェードインで出てくる


step1.png


step2.png


step3.png


解説

動画が重くて動画が出せなかったのですが、ふわっとヘッダーが出ます。
pos.topでmain要素の高さを取得していますが、任意の高さをpx指定することもできます。

htmlがスクロールの関係上長くなっていますが、javascript自体はコンパクトにかけるので難易度は優しい方かなと思います。

他にもいろいろヘッダーの出し方あるので、今後の記事でも書いていけたらとおもっています。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Progate JavaScript 条件分岐

条件分岐

プログラミングにおいて重要な条件分岐
「ある条件が成り立つときだけある処理を行う」という場面が出てくる

このようなプログラムを条件分岐

script.js
const number =12;

↓yes 定数numberの値が10より大きい?

script.js
console.log("numberは10より大きいです");

のようなものを作りたい。

if文の書き方

if文を用いると「もし〇〇ならば⚫️⚫️を行う」という条件分岐が可能になります。ifの後ろに条件式を書き、それが「成り立つ」場合の処理を{ }のなかに書きます。

下記でイメージ

script.js
if(条件式){

  処理//条件式が成り立てば実行される

}//セミコロンは不要

script.js
const number=12;

if(numberが10より大きい場合){

  console.log("numberは10より大きいです。")

  //条件式が成り立てば実行される
}

if文のコード

実際のコードを見てみましょう
条件式の一例「number > 10」の部分は「定数numberの値が10より大きい」という意味の条件式になります。
定数numberには12が代入されているので、この条件は成り立ち、処理が実行されます。

script.js
const number =12;
if(number>10){
  //「numberの値が10より大きい」という条件
  console.log("numberは10より大きいです")  
}

numberは10より大きいです

if文書くときのポイント

if文を書くときは、インデントするようにしましょう。
console.logの部分でtabキーを1回おすと半角スペース2つ分インデントすることができます。
インデントすると、コードが見やすくなります。

scirpt.js
const number= 12;
if(number > 10){
  console.log("numberは10より大きいです")
//↑tabキーでインデント!
}

演習

script.js
const level = 12;

// 条件式を「level > 10」とするif文を作ってください
if(level>10){
  console.log("レベルが10より大きいです");
}

条件式の出力

if文を使うことができました。次は条件式の部分を詳しく見てみましょう。
先ほどのif文の条件式の部分を出力してみると「true」が出力されます

script.js
const number=12;

//条件式を抜き出す
if(number > 10){
  console.log("numberは10より大きいです");
}
script.js
const number=12;
//抜き出した部分の条件式を出力
console.log(number>10);

コンソール(出力結果)

true

となる

真偽値

先ほどの例で出力された「true」は真偽値と呼ばれる。
真偽値にはtruefalseという2つの値しか存在しない。
条件式
・成り立つと「true」
・成り立たないと「false」
という真偽値に置き換わる

よって条件式を出力すると、trueやfalseが出力される

真偽値-true

script.js
const number =12;
console.log(number >10);

コンソール

true

真偽値-false

script.js
const number =8 ;
console.log(number =10);

コンソール

false

if文と真偽値

if文の条件式が
trueであれば処理が実行され、
falseであれば実行されない
ということがわかります。
if文の条件式がtrueの処理の流れをみていきましょう。

script.js
const number=12;
//条件式が成り立つ
if(number > 10){

  console.log("numberが10より大きいです")

}

script.js
const number=12;

//trueの置き換わる
if(true){

  console.log("numberが10より大きい")
}

大小を比べる演算子

条件式に使った「>」は比較演算子と呼ばれる、大小比較の記号です。
「a<b」は、aの方がbより小さいときtrue,大きいときfalseになります。
また「a<=b」とすると、aの方がbより小さいまたは等しい(つまりb以下のときtrueになります。

script.js
 a<b  ・・・aはbより小さい
 a<=b ・・・aの方が小さいまたは等しい
 a>b  ・・・aはbより大きい
 a>=b ・・・aの方が大きいまたは等しい
script.js
const number=12;
//true
console.log(number<30);

//true
console.log(number<=12);

//false
console.log(number>12);

演習

script.js
const age = 24;

// 「age >= 20」を出力してください
console.log(age>=20);

// 「age < 20」を出力してください
console.log(age<20);

// ageの値が20以上の場合に、「私は20歳以上です」と出力してください
if(age>=20){
  console.log("私は20歳以上です");
}


比較演算子

比較演算子には左と右の値が等しいか調べる
「a===b」はaとbが等しければtrue、等しくなければfalseになる
「a!==b」はその逆

等しいか比べる

scipt.js
a===b ・・・ aとbが等しい
a!==b ・・・ aとbが異なる
script.js
const number=12;
//true
console.log(number===12;)

const name= "john";
//false
console.log(name !== "john");

演習

script.js
const password = "ninjawanko";

// passwordの値が"ninjawanko"の場合、「ログインに成功しました」と出力してください
if (password === "ninjawanko"){
  console.log("ログインに成功しました");
}



// passwordの値が"ninjawanko"でない場合、「パスワードが間違っています」と出力してください
if (password !=="ninjawanko"){
  console.log("パスワードが間違っています");
}

条件が成り立たない場合の処理

numberの値が10より大きくない場合には「10以下です」と出力する方法

scirpt.js
const number=7;

↓定数 numberの値が10より大きい?

Yes

"10より大きいです"

No

"10以下です"

としたい

elseの書き方

if文に「else」を組み合わせると
「もし〇〇なら⚫️⚫️を行うそうでなければ■■を行う」
という処理ができるようになります。

script.js
if(条件式){
  条件がtrueの時の処理
}else{
  条件がfalseの時の処理
}

elseのコード

elseを使った実際のコード

script1.js numberの値が10より大きいかどうかで処理を分けた場合に
ifのみを使用した例

script2.js else文を用いると、1つの条件分岐で同じことを実現できます。

elseを使わない場合→複数のif文が必要

script.js
const number=7;
if(number >10 ){
  console.log("numberは10より大きいです");
}
if(number<=10){
  console.log("numberは10以下です")
}

elseを使う場合→1つの条件紙で成立する

script.js
const number=7;
if(number = 10){
  console.log("numebrは10より大きいです")
}else{
  console.log("numberは10以下です")
}

演習

script.js
const age = 17;

// 条件式が成り立たない場合に「私は20歳未満です」と出力してください
if (age >= 20) {
  console.log("私は20歳以上です");
} else{
  console.log("私は20歳未満です");
}


条件を追加する

「10より大きい」という条件を満たさない中で「5より大きい」という条件で
処理を分岐する方法

script.js
const number=7;

定数numberの値が10より大きい?

Yes

"10より大きいです"

NO

かつ定数numberの値が5より大きい?

Yes

"5より大きい"

No

"5以下です"

としたい

else ifの書き方

ifとelseの間に「else if(条件)」を追加することで、
ifに条件分岐に追加することができます。

script.js
if(条件式1){
  条件式1がtrueの時の処理  
}else if(条件式2){
  条件式1がfalse」、条件式2がtrueの時の処理
}else{
  どちらの条件式もfalseの時の処理
}

else ifのコード

「else if」を使った実際のコードを確認
条件式2がtrueなので「else if」 の中の処理が実行され、
コンソールは以下のようになる

script.js
const number = 7;
if(number = 10){//false
  console.log("numberは10より大きいです")

}else if(number > 5){//true
  console.log("numberは5より大きいです)

}else{
  console.log("numberは5以下です")
}

numberは5より大きいです
script.js
const age = 17;


##演習

// ageの値が10以上20未満のとき、「私は20歳未満ですが、10歳以上です」と出力してください
if (age >= 20) {
  console.log("私は20歳以上です");
} else if(age>=10){
  console.log("私は20歳未満ですが、10歳以上です");
}
 else {
  console.log("私は10歳未満です");
}

かつ

複数の条件を組み合わせる方法
「かつ」について
「かつ」は「&&」→「条件1&&条件2」は「条件1かつ条件2」
という意味で、複数の条件が全てtrueならtrueになる
「10より大きいかつ30より小さい」は「10<x<30」と書くことはできない

かつ - &&
・true && true → true
・true && false → false
・false && true → false
・false && false → false

xが20の時 x>10 && x<30 ・・・true
true true

xが5の時 x>10 && x<30 ・・・false
false true

または

「または」は「||」→「条件1||条件2」は「条件1または条件2」
という意味で、複数の条件のうち1つでもtrueならtrueになる

または - ||
・true || true →true
・true || false →true
・false|| true →true
・false|| false →false

xが5の時 x<10 || x>30 ・・・true
true false

xが20の時 x<10 || x>30 ・・・false
false true

組み合わせの具体例

if文を使った「かつ」の具体例
number31のとき
「number>=10」も「number<100」もともとtrueなので、
処理が実行されます

script.js
const number=31;
if(number >=10 && number <100){
  console.log("numberは2桁です");
}

numberは2桁です。

演習

script.js
const age = 24;

// 指定された条件のif文を作成してください
if(age>=20 && age<30){
  console.log("私は20代です");
}

switch文とは

信号機の色を表す、定数colorの値によって処理をしたい例
ある値によって処理を分岐する場合に
switch文を用いることができる

定数color

ある値によって処理も分けたい!

・値が"緑"→「進めます」
・値が"黄"→「要注意」
・値が"赤"→「ストップ」

switch文の書き方(1)

「switch(条件の値){処理}」戸することでswitch文を使って
colorの値によって処理を分岐させる

switchの書き方

script.js
switch(条件の値){
      //変数や定数など
 :
 :

}//セミコロン不要

具体例

script.js
const color="":

switch(color){
 :    //定数のcolorの値に応じて処理を分岐する
 :
}

switch文の書き方(2)

switch文の中にcaseを追加することで処理を分けることができます
定数colorの値が「赤」であるときに「ストップ!」という文字列が出力される

script.js
switch(条件の値){
  case 値1:
  条件の値値1と等しい時の処理
  break;
  //switch{}のなかにcaseを追加して処理を分ける
}

具体例

script.js
const color ='';
switch(color){
  case"":
  console.log("ストップ!");
  //定数colorの値が'赤'の時に実行される
  break;
}

switchの書き方(3)

switch文では分岐の数だけcaseを追加
caseの値に「黄」が指定されており、定数colorの値が「黄」である
場合には、「要注意」と出力されるようになっている

分岐の数だけ追加する

script.js
switch(条件の値){
  case 値1:
  //「条件の値」が「値1」と等しい時の処理
  break;
  case 値2:
  //「条件の値」が「値2」と等しい時の処理
}
script.js
const color ="";
switch(color){
  case"":
  //定数colorの値が"赤"の時に実行される
     console.log("ストップ");
  break
  case"":
  //定数colorの値が"黄"の時に実行される
     console.log("要注意");
  break
}

switch文の注意点

switch文ではbreakが非常に重要です。
breakとはswitch文を終了する命令
合致したcaseの処理を行なった後、その次のcaseの処理も実行してしまいます。そのため、switch文を使う時にはbreakを忘れないように気をつけましょう。

script.js
const color="";
switch(color){
  case"";
     console.log("ストップ");
  case"";
     console.log("要注意");
  berak;

}

上記のどちらも実行される

コンソール

ストップ!
要注意

演習

script.js
const rank = 2;

switch (rank) {
  case 1:
    console.log("金メダルです!");
    break;

  // rankの値が2のcaseを追加してください
  case 2:
   console.log("銀メダルです!");
   break;

  // rankの値が3のcaseを追加してください
  case 3:
  console.log("銅メダルです!");

  break;

}

switch文-default

switchの条件の値がcaseの値と一致した時、
その部分の処理が実行されます。
caseのどれにも一致しなかった時、default
ブロックが実行されます。
defaultはif文のelseに似たようなもの

script.js
switch(条件の値){
  case 値1:
    :
    :
  case 値2:
    :
    :
  case 値3:
    :
    :
//「条件の値」が値1,値2,値3のどれも異なる時
   default
    処理
    break;

}
script.js


const color=""

switch(条件の値){
  case "":
    :
    :
  case "":
    :
    :
  case "":
    :
    :
//「条件の値」が値1,値2,値3のどれも異なる時
//この処理が実行される
   default
     console.log("colorの値が正しくありません")
    break;
}

switch文 -defalut

このようなswitch文の性質を利用すると、if,elseによる分岐が多く
複雑な場合、switch文で書き換えるとシンプルで読みやすいコードにできます。

script.js
  if(条件式){
    処理
}else if (条件式2){
  処理
}else{
  処理
}
script.js
switch(条件の値){
  case 値1:
    処理
    break;
  case 値2:
    処理
    break;
  case 値3:
    処理
    break;
  default
    処理
    break;
}

演習

script.js
const rank = 5;

switch (rank) {
  case 1:
    console.log("金メダルです!");
    break;
  case 2:
    console.log("銀メダルです!");
    break;
  case 3:
    console.log("銅メダルです!");
    break;
  // defaultの処理を追加してください
  default:
    console.log("メダルはありません");
   break;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript で collect と scan

JavaScript には filter, map, reduce などの配列を扱う便利な関数がいくつかありますが,他のモダンな言語にはあるけど JavaScript にはない関数というのもあります。今回はその中で collectscan という関数を自作してみます。

collect

map と flat を合わせたようなものです。
各要素に対して指定された関数を適用し,結果を連結したリストを返します。

参考:https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-listmodule.html#collect

(2020/10/26 追記)
コメントいただきました。普通に同じ意味の flatMap というのがありますね。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap
ランタイムが古くて使えないなどの理由がない限りは必要ないですね。
※ また,その場合でも「大きな配列の場合は避けるべきである」と上記リンクで警告されてます。

ソースコード

const collect = (list, fn) => (
  list && list.map(fn).reduce((acc, x) => acc.concat(x), [])
);

Array.prototype.collect = function(fn) {
  return collect(this, fn);
}

実行例

[3, 4, 5].collect(x => [...new Array(x).keys()].map(x => x + 1))
// > [1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5]

任意の数を受け取ると1からその数までの配列を生成する関数を適用しています。
適用する関数では 3 なら [1, 2, 3] が,4 なら [1, 2, 3, 4] が返りますが,collect はそれらを展開した結果を返します。

scan

fold (reduce) に似ていますが,最終的な結果だけでなく,途中経過の結果の配列を返します。

参考:https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-listmodule.html#scan

ソースコード

const scan = (list, fn, initialValue) => (
  list && list.reduce(
    ([acc, scanAcc], cur, idx, src) => {
      const ret = fn(acc, cur, idx, src);
      scanAcc.push(ret);
      return [ret, scanAcc];
    },
    [initialValue, []]
  )[1]
);

Array.prototype.scan = function(fn, initialValue) {
  return scan(this, fn, initialValue);
}

実行例

[1, 2, 3, 4, 5].scan((a, x) => a + x, 0)
// > [1, 3, 6, 10, 15]

アキュムレーター a に各要素 x を加算する関数と初期値 0 を指定します。

 0 + 1 =  1
 1 + 2 =  3
 3 + 3 =  6
 6 + 4 = 10
10 + 5 = 15

このような順に計算されていきます。fold (reduce) では,最終結果 15 が返却されますが,scan の場合は途中結果も含めて各要素に適用した結果が配列として返却されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】括弧表記{}の引数利用は分割代入だった

概要

ES2015の{}←コイツの記法で惑わされたので自分の理解の範囲でまとめる。
CやJavaは嗜んでいた私。JavaScriptを勉強し始めて現れた{ hoge }の表記。{}ってナニ?
で、調べた。

分割代入

{}で囲うとそのプロパティの値を代入できる

{ a } = { a: 1, b: 2 } //a=1
{ a, b } = { a: 1, b: 2 } //a=1, b=2

なお分割代入は配列でも使える。
分割代入 - JavaScript | MDN

プロパティ名の省略

変数やメソッドをオブジェクトに入れると、プロパティを省略できる。
プロパティ名はその変数名,メソッド名。

  const a = 1
  const b = {a} //b = { a: 1 }

  const c = x => {
    console.log({x})
  }
  c(a) //{ x: 1 }
  c(b) //{ x: { a: 1 } }

引数で{}を使うと分割代入

分割代入もプロパティ名省略も理解したぞーと思ったらここがややこしい。

  const a = 1
  //プロパティ名省略
  const b = { a }

  //引数の{}は分割代入
  const c = ({ a }) => {
    console.log(a) //1

    //プロパティ名省略
    console.log({ a }) //{ a: 1 }
  }

プロパティ名省略の部分が分割代入のに見えたり見えなかったりでつまずいたのでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】仮引数での括弧表記{}の利用は分割代入だった

概要

ES2015の{}←コイツの記法で惑わされたので自分の理解の範囲でまとめる。
CやJavaは嗜んでいた私。JavaScriptを勉強し始めて現れた{ hoge }の表記。{}ってナニ?
で、調べた。

分割代入

{}で囲うとそのプロパティの値を代入できる

{ a } = { a: 1, b: 2 } //a=1
{ a, b } = { a: 1, b: 2 } //a=1, b=2

なお分割代入は配列でも使える。
分割代入 - JavaScript | MDN

プロパティ名の省略

変数やメソッドをオブジェクトに入れると、プロパティを省略できる。
プロパティ名はその変数名,メソッド名。

  const a = 1
  const b = {a} //b = { a: 1 }

  const c = x => {
    console.log({x})
  }
  c(a) //{ x: 1 }
  c(b) //{ x: { a: 1 } }

仮引数で{}を使うと分割代入

分割代入もプロパティ名省略も理解したぞーと思ったらここがややこしい。

  const a = 1
  //プロパティ名省略
  const b = { a }

  //仮引数の{}は分割代入
  const c = ({ a }) => {
    console.log(a) //1

    //プロパティ名省略
    console.log({ a }) //{ a: 1 }
  }

プロパティ名省略の部分が分割代入のに見えたり見えなかったりでつまずいたのでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】amCharts4を用いたグラフ描画における第2縦軸の作成及び日本語化 他

はじめに

本記事では、JavaScriptのグラフ描画ライブラリのamChartを用いて、複数軸の線グラフの実装する際の第2縦軸の作成方法や日本語化などのカスタム方法を共有します。

開発環境

Ruby 2.5.1
Rails 5.2.4.4

amChartsとは

amChartsは、javascriptのグラフ描画ライブラリで、様々な種類の高機能なグラフを描画することができます。

公式リファレンス:https://www.amcharts.com/docs/v4/

日本語の情報が少ないため、カスタマイズする際は、公式リファレンスを参照することをおすすめします。

完成イメージ

現在、体重と体脂肪率を記録してグラフ化する機能を持つアプリを開発中で、
1つのグラフに横軸を日付、第1縦軸に体重、第2縦軸に体脂肪率を描画するため、amCharts4を使用しました。

下図のグラフが完成イメージとなります。
ezgif-6-ef32c8681700.gif

  • 横軸:日付、第1縦軸:体重、第2縦軸:体脂肪率
  • カーソル上のデータをTooltipで表示
  • スクロールバーで拡大
  • タイトルをマウスホバーするとTooltipを表示

実装手順/解説

amChartsの導入

amChartsの導入方法はこちらの記事が参考になります。
amcharts 4 Demos を使ってグラフを作成

横軸の値が不連続な場合のグラフの作成

本記事の横軸の値が不連続になる場合のグラフの作成は、下記の記事を参考にさせていただきました。
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。

デモデータの準備

csvファイルをseedして以下のようなデモデータを準備します。

id date weight body_fat_percentage
1 2020/06/08 72 15
2 ・・・ ・・・ ・・・

完成サンプルコード

解説の前にサンプルコードを貼っておきます。
Rails側の記述は今回割愛します。

record.html.erb
<style>
  #chartdiv {
    width: 700px;
    height: 300px;
  }
</style>

//必要なJSファイルの読み込み
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script>

<script>
am4core.ready(function() {

am4core.useTheme(am4themes_animated);

var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.dateFormatter.language = new am4core.Language();
chart.dateFormatter.language.locale = am4lang_ja_JP;
chart.language.locale["_date_day"] = "MMMdd日";
chart.language.locale["_date_year"] = "yyyy年";

const weights = <%== JSON.dump(@weights) %>;
const body_fat_percentages = <%== JSON.dump(@body_fat_percentages) %>;
const dates = <%== JSON.dump(@dates) %>;

var firstDate = new Date(dates[0])
var lastDate = new Date(dates.slice(-1)[0])
var termDate = (lastDate - firstDate) / 1000 / 60 / 60 / 24 + 1

function generateChartData() {
  var chartData = [];
  for (var j = 0; j < weights.length; j++ ) {
    for (var i = 0; i < termDate; i++ ) {
      var newDate = new Date(firstDate)
      newDate.setDate(newDate.getDate() + i);
      if ((new Date(dates[j])) - (newDate) == 0 ){
        weight = weights[j]
        body_fat_percentage = body_fat_percentages[j]
        chartData.push({
          date1: newDate,
          weight: weight,
          date2: newDate,
          body_fat_percentage: body_fat_percentage
        });
      }
    }
  }
  return chartData;
}

chart.data = generateChartData();

//グラフタイトルの設定
var title = chart.titles.create();
title.text = "体重・体脂肪率の推移"; //グラフタイトルの設定
title.fontSize = 15; //グラフタイトルのフォントサイズの設定
//タイトルをマウスホバーした際に表示させるTooltipの表示内容設定
title.tooltipText = "スクロールバーで拡大できます。"; 

//第1横軸の設定
var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
dateAxis.renderer.grid.template.location = 0;
dateAxis.renderer.labels.template.fill = am4core.color("#ffffff");

//第2横軸の設定
var dateAxis2 = chart.xAxes.push(new am4charts.DateAxis());
dateAxis2.tooltip.disabled = true; //Tooltipの非表示設定
dateAxis2.renderer.grid.template.location = 0;
dateAxis2.renderer.labels.template.fill = am4core.color("#000000");

//第1縦軸の設定
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.tooltip.disabled = true;
valueAxis.renderer.labels.template.fill = am4core.color("#e59165");
valueAxis.renderer.minWidth = 60;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "kg";
});
valueAxis.renderer.fontWeight = "bold"; //軸の値を太字に変更

//第2縦軸の設定
var valueAxis2 = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis2.tooltip.disabled = true;
valueAxis2.renderer.grid.template.strokeDasharray = "2,3";
valueAxis2.renderer.labels.template.fill = am4core.color("#dfcc64");
valueAxis2.renderer.minWidth = 60;
valueAxis2.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});
valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定
valueAxis2.renderer.fontWeight = "bold"; //軸の値を太字に変更

//第1縦軸用の値の設定
var series = chart.series.push(new am4charts.LineSeries());
series.name = "体重";
series.dataFields.dateX = "date1";
series.dataFields.valueY = "weight";
series.tooltipText = "{valueY.value}kg";
series.fill = am4core.color("#e59165");
series.stroke = am4core.color("#e59165");
series.smoothing = "monotoneX";
series.strokeWidth = 2;

//系列のポイントの設定(第1縦軸)
var bullet = series.bullets.push(new am4charts.Bullet());
var circle = bullet.createChild(am4core.Circle);
circle.width = 5;
circle.height = 5;
circle.horizontalCenter = "middle";
circle.verticalCenter = "middle";

//第1縦軸用の値の設定
var series2 = chart.series.push(new am4charts.LineSeries());
series2.name = "体脂肪率";
series2.dataFields.dateX = "date2";
series2.dataFields.valueY = "body_fat_percentage";
series2.yAxis = valueAxis2;
series2.xAxis = dateAxis2;
series2.tooltipText = "{valueY.value}%"; //ツールチップの表示設定
series2.fill = am4core.color("#dfcc64"); //ツールチップの色
series2.stroke = am4core.color("#dfcc64"); //グラフの線の色
series2.smoothing = "monotoneX";
series2.strokeWidth = 2;

//系列のポイントの設定(第2縦軸)
var bullet2 = series2.bullets.push(new am4charts.Bullet());
var circle2 = bullet2.createChild(am4core.Circle);
circle2.width = 5;
circle2.height = 5;
circle2.horizontalCenter = "middle";
circle2.verticalCenter = "middle";

chart.scrollbarX = new am4core.Scrollbar(); //スクロールバーの設定

//カーソルの設定
chart.cursor = new am4charts.XYCursor();
chart.cursor.xAxis = dateAxis2;

//凡例の設定
chart.legend = new am4charts.Legend();
chart.legend.parent = chart.plotContainer;
chart.legend.zIndex = 100;
chart.legend.position = "top";
chart.legend.contentAlign = "right";

//グリッド線の設定
valueAxis2.renderer.grid.template.strokeOpacity = 0.07;
dateAxis2.renderer.grid.template.strokeOpacity = 0.07;
dateAxis.renderer.grid.template.strokeOpacity = 0.07;
valueAxis.renderer.grid.template.strokeOpacity = 0.07;

});
</script>

<div id="chartdiv"></div>

解説

複数縦軸の設定

サンプルコードの通り、複数軸のグラフの場合、各軸及び各値の設定が必要になります。
それぞれの使用するデータなどの設定を行います。

  • 第1横軸:dateAxis
  • 第2横軸:dateAxis2
  • 第1縦軸:valueAxis
  • 第2縦軸:valueAxis2
  • 体重:series
  • 体脂肪率:series2

日本語化

横軸が日付の場合、デフォルトの表記が米国式のため下図のように英語表記になります。
スクリーンショット 2020-10-25 22.02.52.png

そのままでも問題は無いのですが、もし「◯月◯日」という表記にしたい場合は、以下の設定を追加します。
標準の翻訳設定では、例えば「Aug」を「8月」に翻訳はできますが、何日の方は「〇〇日」とは翻訳されないため、独自ルールを以下のように追加します。年も同様に行えます。

<script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script> //localeファイルの呼び出し
<script>
//中略
chart.dateFormatter.language = new am4core.Language(); //標準の翻訳設定
chart.dateFormatter.language.locale = am4lang_ja_JP; //標準の翻訳設定
chart.language.locale["_date_day"] = "MMMdd日"; 独自ルールで上書き
chart.language.locale["_date_year"] = "yyyy年"; 独自ルールで上書き
//中略
</script>

【参考リンク】
 https://www.amcharts.com/docs/v4/concepts/locales/
 https://github.com/amcharts/amcharts4/blob/master/src/lang/ja_JP.ts

第2縦軸の設定

デフォルトの設定ですと第1縦軸と第2縦軸は両方左側にあります。
スクリーンショット 2020-10-25 22.14.34.png
少し見にくいので、第2縦軸を右側に変更したい時は、以下の設定を追加します。

valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定

他の追加設定を紹介

  • データポイントの設定

各系列毎にデータポイントの設定が行えます。circleをsquareに変えると四角に変更できます。

var bullet = series.bullets.push(new am4charts.Bullet());
var circle = bullet.createChild(am4core.Circle);
circle.width = 5;
circle.height = 5;
circle.horizontalCenter = "middle";
circle.verticalCenter = "middle";

【参考リンク】
 https://www.amcharts.com/docs/v4/concepts/bullets/

  • グラフの線を曲線に変更
series.smoothing = "monotoneX";
  • 色の変更
series2.fill = am4core.color("#dfcc64"); //ツールチップの色
series2.stroke = am4core.color("#dfcc64"); //グラフの線の色

まとめ

amChartsを使うと高機能なグラフを描画できます。
折れ線グラフ以外にも様々なグラフを作ることができます。
日本語の情報が少ないので、カスタマイズしたい場合は、公式リファレンスを参照することをおすすめします。

参考URL

amcharts 4 Demos を使ってグラフを作成
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「トップへ戻る」ボタンと「100vhだけ・一番下まで進む・戻る」ボタン

「トップへ戻る」ボタンと「100vhだけ・一番下まで進む・戻る」ボタン

jQueryの「トップへ戻る」ボタンのやり方は鬼のように出てくるのですけど、
下に行くのも、マウスホイールぐるぐる回して地味にめんどくさいときありませんか?
ぼくはめんどくさいときがあったのでやってみました。


See the Pen
pobezbL
by sarap422 (@sarap422)
on CodePen.


似たようなこと考えた方はやはりいたようで、
「scrollBottom」で検索するとすぐ出てきしたが、
なかなかわからなかったのは、「100vh」だけページ送り・戻りする方法。

結論として言うと、「100vh = window.innerHeight」で、

// 一番下まで
document.body.clientHeight
// 100vh下
window.pageYOffset + window.innerHeight
// 100vh上
window.pageYOffset - window.innerHeight

で、実装することができました。わーい

参考

JavaScriptで画面サイズを取得・変更する方法
https://uxmilk.jp/28500

ついでに「mix-blend-mode」というのが便利そうだったので、
調べてみましたが「exclusion」が楽そうだなーと思いました。

See the Pen ZEOLZwx by sarap422 (@sarap422) on CodePen.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptテスティングフレームワークのJestとは

プログラミング勉強日記

2020年10月25日
Babel、Typescript、Node、React、Angularなどで使えるテストの存在を知ったのでまとめる。

Jestとは

 FaceBook社がOSSとして開発しているJavaScriptのユニットテストのためのツール。公式のドキュメントはこちらで日本語のドキュメントもある。
 特別な設定をすることなく、他のライブラリを追加することなく、誰でもすぐにユニットテストを書けるのが特徴。

特徴

 公式ドキュメントにあるようにJestの特徴は以下のようになる。

  • zero config
  • snapshots
  • isolated
  • great api
  • fast and safe
  • code coverage
  • easy mocking
  • great exceptions

zero config

 上でも述べているが、JestはJavaScriptプロジェクトにおいて細かい設定が不要なので、すぐにユニットテストを書くことができる。

snapshots

 テストと一緒にインラインに埋め込んだ状態で表示できるスナップショット機能で大きなオブジェクトを容易に追跡できるテストを作成できる。

isolated(独立的)

 パフォーマンスを最大化するために、別々のプロセスで実行してテストを並列化する。

great api

 itからexpectまで、Jestにはすべてのツールキットが1つにまとまっている。ドキュメント化されてメンテナンスもできている。

fast and safe

 テストが一意なグローバル状態であり、Jestは安全にテストを並列実行することができる。また、開発効率を上げるために前に失敗したテストから実行して、テストファイルの所要時間に基づいて再整理する。

code coverage

 フラグの--coverageを指定することで、追加の設定なしでコードガバレッジを生成する。Jestは最後のテストのファイルを含むプロジェクト全体からコードガバレッジ情報を収集できる。

easy mocking

 Jestはテストにおけるインポートに独自のリゾルバを使用することで、テストコード外のオブジェクトを簡単にモックできる。豊富なモック機能APIでモックされたインポートを利用して、読みやすいテスト構文で関数呼び出しをできる。

great exceptions

 テストが失敗したときの理由をわかりやすく表示する。

参考文献

第1回 環境の準備とテストの実行
公式ドキュメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Javascript】onclickイベントが呼び出されない

概要

railsでアプリケーションを作成中。
Javascriptを使用してボタンのクリックで要素の表示/非表示を切り替えたいが、クリックをしても動かない。
コードは以下のとおり。

html.erb
  <button type=button id="info-btn">ログイン情報</button>  
  <div class="login-info" id="js-login-info">
   <p>アドレス:hoge パスワード:hogehoge</p>
  </div>
css
.login-info { 
  display: none;
}
click.js
document.getElementById("info-btn").onclick = function(){
  const loginInfo = document.getElementById("js-login-info");
  if(loginInfo.style.display=="block"){
    loginInfo.style.display = "none";
  }else{
    loginInfo.style.display = "block";
  };
};
application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("../click")ここで該当のjsファイルを読み込む

display: noneで要素を見えないようにしておき、クリックしたタイミングで値をblockに切り替えるようにしていたがいくらクリックしても動かず...。
コンソールを確認したところUncaught TypeError: Cannot set property 'onclick' of nullというエラーが表示されていた。

解決方法

個別でjsファイルをコンパイルして参照することで解決。下記の記事を参考にさせていただきました!
Ruby on RailsにおけるJavascriptファイルの取り扱い(Rails6)
具体的には以下の点を変更しました。
・ jsファイルの場所をapp/javascript/click.jsからapp/javascript/packs/click.jsに変更
・ 該当ビューファイル(new.html.erb)の一番下に<%= javascript_pack_tag 'click' %>を追加

検証ツールで確認すると<%= javascript_pack_tag 'click' %>を追加した箇所にscriptタグと共にコンパイルされたjsファイルが読み込まれていました。

script.png

最後に

headタグ内にある<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>でjavascriptファイルを読み込んでいるため、bodyタグの中身が読み込まれる前にjsファイルが読み込まれたことが原因でエラーが起きていたということだろうか。
問題は解決できたが自分の中に落とし込めていないため間違っている点がありましたらご指摘お願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【javascript】数値文字列の先頭の0を削除(ゼロサプレス)

前書き

ゼロパディングの逆がしたくなりました。
以下のように変換。

'000001230' => '1230'

(追記)

投稿後に検索したら以下でもできると知りました。

Number('000001230');    //1230

状況が合えばこのパターンも良いですね。

コード(3パターン)

正規表現

const suppressZero1 = str => {
    //略
    return str.replace(/^0+/, '');
};
console.log( suppressZero1('000001230') );  //'1230'

配列化

const suppressZero2 = str => {
    //略
    const idx = str.split('').findIndex(x => x!=='0');
    return str.slice(idx);
};
console.log( suppressZero2('000001230') );  //'1230'

while

const suppressZero3 = str => {
    //略
    let idx = 0;
    while (str.charAt(idx)==='0') idx++;
    return str.slice(idx);
};
console.log( suppressZero3('000001230') );  //'1230'

速度

私の環境での処理時間は
(速<遅)while =< 配列化 << 正規表現
の順番でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NimでVue.js風のCanvas APIライブラリを作っている話

はじめに

こんにちは。高校2年生のmomeemtです。
今回は、Nimというプログラミング言語を使って、Vue.js風のCanvas(HTML5 APIの1つ)ライブラリ、「Blackvas」を開発している話についてまとめていきます。

Canvasとは、<canvas>要素とJavaScriptを用いてグラフィックを描画するための、HTML5 APIの1つです。詳しくはCanvas API - Web API | MDNをご覧ください。本記事では、Canvasそのものについての解説は、必要以外は省きます。

また、Nimとは2008年に最初の開発バージョンが、2019年に安定版がリリースされたモダンなシステムプログラミング言語です。Pythonのような構文を採用した一見インタプリタ言語のような言語ですが、静的型付けで、C言語やObjective-C、JavaScriptにトランスパイルできます。Nimについては、公式サイトや、Qiitaに投稿されているNimを知ってほしいなどの解説記事をお読みください。

本ライブラリはGitHubで公開しています。
Nimbleにも公開しておりますので、

nimble install blackvas

でイントールして使い始めることができます。
また、開発途中のライブラリであり、破壊的変更が行われることがあります。

動機

Canvas APIにおけるライブラリの必要性

Canvas APIは、かなり低レベルなAPIです。パスの設定・描画、気持ち程度に四角形などの描画が提供されていますが、直接Canvas APIのみを利用して開発することは憚られます。
ですから、EaselJSPaper.jsなどのサードパーティ製のJavaScriptライブラリが開発されてきました。
では、それらのコードを眺めてみましょう。

EaselJS
var stage = new createjs.Stage("stage");
var shape = new createjs.Shape();
shape.graphics.beginFill("blue");
shape.graphics.drawCircle(0, 0, 100);
stage.addChild(shape);
Paper.js
const circle = Shape.Circle(0, 0, 100);
circle.fillColor = 'blue';
paper.view.draw();

これらは、青い円を描画するだけのプログラムです。シンプルな処理ですが、どちらのライブラリからも方向性は感じ取ることはできます。
Canvas APIを愚直に用いると、

CanvasAPI
const canvas = document.getElementById('canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.arc(100, 100, 100, 0 * Math.PI / 180, 360 * Math.PI / 180, false)
context.fillStyle = 'blue'
context.fill()

このようなコードが必要になります。Canvas APIは円の描画関数を提供しませんから、円弧のPathを動くarc関数を用いて描くことになります。

この程度ならCanvas APIでも良い、と感じられるでしょう。しかし、Canvas APIは本当に上のようなシンプルなパスを描くことだけを提供した良くも悪くも最低限のライブラリです。例えば、Canvas APIはHTMLサイドがCanvas要素1つだけで、ほとんどJavaScriptが描画を担うため、クリックイベントを始めとしたイベント系は描画した要素に対して適用させることはできません。

サードパーティライブラリはそのような点もサポートしていることが多いため、描画することが増えるにつれて、ライブラリの存在が頼りになることでしょう。

既存ライブラリの問題点

しかし、私は既存ライブラリには問題点があると考えました。

Canvas APIで行う描画はアニメーションなども用いられ、複雑で、込み入ったものになりがちです。しかし、既存ライブラリは描画イベントに関してはサポートを行いますが、設計についてはサポートを行いません。
開発者自身で、複雑なCanvasプログラムを保守していかなければならないのです。

話はWeb開発になりますが、近年では、JQueryで行われやすい手続き型プログラミングは規模の拡大と主に保守の困難さから敬遠されるようになりました。代わりに、Vue.jsやReactのような宣言型UIライブラリが台頭し、誰も彼もがこれらを使ってWebフロントエンド開発を行うようになりました。

宣言型プログラミングというのは、ある対象の性質を宣言することでプログラムを構築する手法です。
関数型プログラミングも、宣言型プログラミングに属しています。特に、Reactは関数型プログラミングによく影響を受けているように感じます。

Vue.jsは、Evan You氏が開発した人気のある宣言型UIライブラリのひとつです。

<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello, Vue.js'
    }
  }
}
</script>

Vue.jsの宣言的UI・コンポーネント志向は、Canvasにも適用できるのではないかと思いました。特に、図形の描画において同じような素材 = コンポーネントが大量に存在するでしょうから、コンポーネント指向とも親和性が高いはずだと考えました。

そのことから、ユーザーが利用しやすい高レベルの関数を提供することと、宣言的UI・コンポーネント指向を提供することで、より設計しやすく簡潔に記述できることを目指したライブラリを開発し始めました。

Blackvasのサンプルコード

ここでBlackvasの使い方について事細かく説明するのは野暮ですから、サンプルコードをもとに簡単に解説します。

import Blackvas, random

Blackvas:
  shapes:
    shape MyCircle:
      circle(100f, 100f, 100f)

  style:
    id red:
      color = PrimaryColor.red

  data:
    var
      dx = 5.0
      dy = 5.0

  methods:
    proc randomWalk(context: var VasContext) =
      context.x += dx
      context.y += dy
      var dPos = rand(5.0) + 5.0
      if context.x <= 0:
        dx = dPos
      elif context.y <= 0:
        dy = dPos
      elif context.x + 200.0 >= width:
        dx = -dPos
      elif context.y + 200.0 >= height:
        dy = -dPos

  data:
    const redId = "red"

  view:
    MyCircle:
      id = redId
      @animation = randomWalk

図形がコンポーネントとしてshapesで切り出されています。
定義されたshapeは、view内から呼び出すことで画面に描画されます。

図形の色などスタイルを変更したい場合には、styleに記述します。図形はidclassを持つことができるので、それらを用いるか、shape名からスタイルを適用できます。

methods内にあるプロシージャ(他言語の関数のことをNimではプロシージャと呼びます)は、VasContext型のパラメータを受け取ります。これは、呼び出し元のshapeのデータを保持しており、座標などのデータを操作することができます。

shape呼び出しの中にある@animationは、アニメーションを実行します。正確には、20msごとに設定したプロシージャが呼び出されます。
また、サンプル内にはありませんが@clickを用いることで、shapeごとに独立したクリックイベントを発行します。

Blackvasの実装

Blackvasの雰囲気を感じ取っていただけたでしょうか。
それでは、これらの実装について触れていきます。

NimからCanvasを利用する

Nimは、JavaScriptにトランスパイルできると序文で説明しましたが、JavaScriptの資産を利用することもできます。

canvas.nim
import dom

type
  Event* = ref EventObj
  EventObj {.importc.} = object of RootObj
    target*: Node
  ...

Canvas* = ref CanvasObj

CanvasObj {.importc.} = object of dom.Element
  width* : float
  height* : float

...

# methods
proc addEventListener*(et: Canvas, ev: cstring, cb: proc(ev: Event), useCapture: bool = false) {.importcpp.}
proc addEventListener*(et: Window, ev: cstring, cb: proc(ev: Event), useCapture: bool = false) {.importcpp.}

proc arc*(c: CanvasContext2d, x: float, y: float, radius: float, startAngle: float, endAngle: float, anticlockwise: bool = false) {.importcpp.}

...

一部省略していますが、Blackvasのcanvas.nimというコードです。
標準ライブラリからDOMを扱えるdomモジュールが提供されているので、import domで読み込めます。
また、コンパイラに補足的に情報を付与するプラグマ{.importcpp.}を用いて、JavaScriptからaddEventListenerarcなどの関数を読み込んでいます。

ちなみに逆も同様で、JavaScriptからNimのコードを呼び出すこともできます。

仮想Canvas

Canvasでは、各々の要素のパラメータを変化させてアニメーションを行うことがよくあります。
Blackvasでもそれをサポートしているので、コンパイル時に出力されるCanvas情報が確定してしまうと、アニメーションを発生させることができません。
そこで、仮想Canvasという、実際に画面に描画される前にJSONの形でshapeとviewに出力される
shapeの情報を保持して、その情報をもとに描画されます。そうすることでCanvasのアニメーションは再描画を繰り返して実現しますから、仮想Canvasの更新 + 画面の再描画を行うだけで実現できることになります。

shape.nim
macro Blackvas*(body: untyped): untyped =

  result = """
import json, strutils, math, dom, tables
var
  canvas*: Canvas
  context*: CanvasContext2d
  globalEvent*: Blackvas.Event
  virtualCanvas* = %* { "virtual_canvas": {}, "shapes": {}, "styles": {} }
""".parseStmt

  result.add quote do:
    proc removeDoubleQuotation(str: string): string =
      result = str[1..str.len-2]
    proc drawShape (context: CanvasContext2d, shapesArr: JsonNode, baseX: float, baseY: float) =
      for obj in shapesArr:
        let rawObjFunc = obj["func"].pretty
        let objFunc = rawObjFunc[1..rawObjFunc.len-2]
        case objFunc:
        of "style_color":
          let color = removeDoubleQuotation(obj["color"].pretty)
          context.fillStyle = color
        of "rect":
          let
            x = obj["x"].pretty.parseFloat + baseX
            y = obj["y"].pretty.parseFloat + baseY
            width = obj["width"].pretty.parseFloat
            height = obj["height"].pretty.parseFloat
          context.beginPath()
          context.rect(x, y, width, height)
          context.fill()
        of "text":
          let
            value = removeDoubleQuotation(obj["value"].pretty)
            x = obj["x"].pretty.parseFloat + baseX
            y = obj["y"].pretty.parseFloat + baseY
          context.strokeText(value, x, y)
          context.fillText(value, x, y)
        of "triangle":
          let
            v1x = obj["x1"].pretty.parseFloat + baseX
            v1y = obj["y1"].pretty.parseFloat + baseY
            v2x = obj["x2"].pretty.parseFloat + baseX
            v2y = obj["y2"].pretty.parseFloat + baseY
            v3x = obj["x3"].pretty.parseFloat + baseX
            v3y = obj["y3"].pretty.parseFloat + baseY
          context.beginPath()
          context.moveTo(v1x, v1y)
          context.lineTo(v2x, v2y)
          context.lineTo(v3x, v3y)
          context.fill()
        of "circle":
          let
            x = obj["x"].pretty.parseFloat + baseX
            y = obj["y"].pretty.parseFloat + baseY
            r = obj["r"].pretty.parseFloat
          context.beginPath()
          context.arc(x, y, r, 0, 2 * math.PI)
          context.fill()

    proc draw (context: CanvasContext2d) =
      context.clearRect(0, 0, canvas.width, canvas.height)
      let virtualCanvasObjects = virtualCanvas["virtual_canvas"]
      let shapesObjects = virtualCanvas["shapes"]
      let styleObjects = virtualCanvas["styles"]
      for item in virtualCanvasObjects.pairs:
        if not item.val.hasKey("shape"):
          continue
        let shapeName = removeDoubleQuotation(item.val["shape"].pretty)
        var
          idName = ""
          className = ""
          baseX = 0.0
          baseY = 0.0
        if item.val.hasKey("id"):
          idName = "#" & removeDoubleQuotation(item.val["id"].pretty)
        if item.val.hasKey("class"):
          className = "." & removeDoubleQuotation(item.val["class"].pretty)
        if item.val.hasKey("x"):
          baseX = item.val["x"].getFloat.float
        if item.val.hasKey("y"):
          baseY = item.val["y"].getFloat.float
        context.font = "24px Arial"
        context.fillStyle = "#000000"
        context.textAlign = "start"
        let shapeArray = shapesObjects[shapeName]
        let styleArrayById = styleObjects[idName]
        for obj in styleArrayById:
          let
            style = removeDoubleQuotation(obj["style"].pretty)
            value = removeDoubleQuotation(obj["value"].pretty)
          case style:
          of "color":
            context.fillStyle = value
        drawShape(context, shapeArray, baseX, baseY)

    window.addEventListener("load",
      proc (event: Event) =
        if document.getElementById(canvasId) == nil:
          let blackvas = document.getElementById("Blackvas")
          canvas = dom.document.createElement("canvas").Canvas
          canvas.id = canvasId
          canvas.height = height
          canvas.width = width
          blackvas.appendChild(canvas)
        else:
          canvas = document.getElementById(canvasId).Canvas

        context = canvas.getContext2d()

        when Debug:
          echo pretty virtualCanvas
        draw(context)
        echo "Hello, Blackvas ;)"
    )
  result.add body

これは、Blackvasマクロです。Nimのマクロは、C言語のプリプロセッサのような単なる項置き換えマクロではなく、抽象構文木を構築して返すことができるので高い表現力を持っています。

virtualCanvasは、JSON型の変数で、その要素のvirtual_canvasは、view層で呼び出された描画されるshape、shapesは定義されたshape全て、stylesは定義されたstyle全てが格納されます。

吐き出されるdrawプロシージャは、仮想Canvasの情報をもとにCanvas contextに適用しています。そこで呼び出されるdraw_shapeプロシージャは、実際にshapesの要素をCanvasのcontextに書き出します。

BlackvasマクロはBlackvasプログラムの一番最初で呼び出されることを前提にしています。なぜなら、ここで最初にvirtual_canvas変数が定義されることを他のマクロは期待しているからです。
ユーザーは、コンポーネント指向や宣言的UIの恩恵を受けるために、いくつかの制約を守らなければいけないようになっています。

クリックイベントの実装

開発でおそらく最も苦労したことは、クリックイベントの実装です。
現在は、Blackvasが提供している図形が四角形、三角形、丸のみですが、今後自由に記述できるパスなどを追加したら実装が地獄になる予感がします。

Canvas領域の中で図形がどの座標に位置しているか、クリックした座標がそれらの図形の内側の存在するかを判定しています。
getAddListenderClickEventは、shapesマクロから吐き出されるプロシージャです。処理自体はとても単純なのですが、このプロシージャもNimNodeという構文木を返すので、実際にされるコードまで3段階あり、途中でどの段階のコードを考えているのか混乱することが多々ありました。
プロシージャ名を受け取って呼び出さなければならないので、構文木を扱う段階まではずっと構文木を返すコードを考え続けなければならずこのような実装になりましたが、もう少しあったんじゃないかとも思います。

proc getAddListenerClickEvent (procedureName: NimNode, shape: string, shapeInstanceName: string): NimNode =
      let shapeNimNode = shape.parseStmt
      var devideShapes = newSeq[string]()
      for sentence in shapeNimNode:
        let shapeKind = sentence[0].repr
        case shapeKind:
        of "rect":
          devideShapes.add (%* {
            "kind": shapeKind,
            "x": sentence[1].floatVal.float,
            "y": sentence[2].floatVal.float,
            "width": sentence[3].floatVal.float,
            "height": sentence[4].floatVal.float
          }).pretty
        of "triangle":
          devideShapes.add (%* {
            "kind": shapeKind,
            "x1": sentence[1].floatVal.float,
            "y1": sentence[2].floatVal.float,
            "x2": sentence[3].floatVal.float,
            "y2": sentence[4].floatVal.float,
            "x3": sentence[5].floatVal.float,
            "y3": sentence[6].floatVal.float
          }).pretty
        of "circle":
          devideShapes.add (%* {
            "kind": shapeKind,
            "x": sentence[1].floatVal.float,
            "y": sentence[2].floatVal.float,
            "r": sentence[3].floatVal.float
          }).pretty

      result = quote("@@") do:
        import json
        window.addEventListener("load",
          proc(event: Event) =
            var canvas: Canvas
            if document.getElementById(canvasId) == nil:
              let blackvas = document.getElementById("Blackvas")
              canvas = dom.document.createElement("canvas").Canvas
              canvas.id = canvasId
              canvas.height = height
              canvas.width = width
              blackvas.appendChild(canvas)
            else:
              canvas = document.getElementById(canvasId).Canvas
            let context = canvas.getContext2d()
            canvas.addEventListener("click", 
              proc (event: Blackvas.Event) =
                let shapeInstance = virtualCanvas["virtual_canvas"][@@shapeInstanceName]
                echo shapeInstance.pretty
                var
                  # Shapeの基準座標
                  baseShapeX = 0.0
                  baseShapeY = 0.0
                if shapeInstance.hasKey("x"):
                  baseShapeX = shapeInstance["x"].getFloat
                if shapeInstance.hasKey("y"):
                  baseShapeY = shapeInstance["y"].getFloat
                let
                  canvasRect = canvas.getBoundingClientRect()
                  # click時のCanvas内における基準座標
                  basePointX = event.clientX - canvasRect.left
                  basePointY = event.clientY - canvasRect.top
                var isClick = false
                var shapes = newSeq[JsonNode]()
                for shapeStr in @@devideShapes:
                  shapes.add shapeStr.parseJson
                for shape in shapes:
                  let
                    rawKind = shape["kind"].pretty
                    kind = rawKind[1..rawKind.len-2]
                  case kind:
                  of "rect":
                    let
                      x = shape["x"].getFloat + baseShapeX
                      y = shape["y"].getFloat + baseShapeY
                      width = shape["width"].getFloat
                      height = shape["height"].getFloat
                      intoCond1 = x <= basePointX and y <= basePointY
                      intoCond2 = (x + width) >= basePointX and (y + height) >= basePointY
                    if intoCond1 and intoCond2:
                      isClick = true

                  of "triangle":
                    # ベクトル係数を計算して三角形内にクリック座標が含まれているか
                    let
                      x1 = shape["x1"].getFloat + baseShapeX
                      y1 = shape["y1"].getFloat + baseShapeY
                      x2 = shape["x2"].getFloat + baseShapeX
                      y2 = shape["y2"].getFloat + baseShapeY
                      x3 = shape["x3"].getFloat + baseShapeX
                      y3 = shape["y3"].getFloat + baseShapeY
                      area = 0.5 * (-y2 * x3 + y1 * (-x2 + x3) + x1 * (y2 - y3) + x2 * y3)
                      sScala = 1 / (2 * area) * (y1 * x3 - x1 * y3 + (y3 - y1) * basePointX + (x1 - x3) * basePointY)
                      tScala = 1 / (2 * area) * (x1 * y2 - y1 * x2 + (y1 - y2) * basePointX + (x2 - x1) * basePointY)
                      scalaDiff = 1 - sScala - tScala
                    if (0 < sScala and sScala < 1) and (0 < tScala and tScala < 1) and (0 < scalaDiff and scalaDiff < 1):
                      isClick = true

                  of "circle":
                    let
                      x = shape["x"].getFloat + baseShapeX
                      y = shape["y"].getFloat + baseShapeY
                      r = shape["r"].getFloat
                      pointDistanceSquare = (x - basePointX) ^ 2 + (y - basePointY) ^ 2
                    if pointDistanceSquare <= r ^ 2:
                      isClick = true
                if isClick:
                  var vasContext = getVasContext(@@shapeInstanceName)
                  @@procedureName(vasContext)
                  restructJson(@@shapeInstanceName, vasContext)
                  draw(context)
            )
        )

展望など

現在、BlackvasはVue.js風の文法や、コンポーネント指向や宣言的UIの恩恵を受けるため、制約を守ったり、大部分をmacroで記述しなければいけないというデメリットがあります。

特に後者は、Nim公式も「プロシージャでどうしても実現できない場合にテンプレートを使い、テンプレートでどうしても実現できない場合にマクロを使いなさい」と呼びかけているほど、マクロは可読性を下げます。
ドキュメントがなければ他のマクロがvirtual_canvasの宣言を前提に動作していることには気づけないでしょう。今後は、テンプレートに置き換えたり、プロシージャに置き換えることで可読性を上げていきたいと考えています。

Canvas APIにはまだまだ可能性があるような気がしています。WebGL APIなども読んでみて、2Dだけでなく3Dの描画ができるように実装を進めたり、Unityのようなゲーム開発エンジンのように開発を進めても面白いなあと思っています。

スクリーンショット 2020-10-25 8.50.07.png

また、Issueを作成している通り、いろいろなアイディアがあり、それを少しずつ実装している状態です。
もし、何かBlackvasをよりよくするようなアイディアがあれば、Issueに書き込んでいただければ大変助かります。

せっかくオープンソースにしているので、新しい開発者が増えるようにドキュメントの整備なども行っていきたいです。

最後に

前回の単位型ライブラリを開発してから、ライブラリを開発することがとても楽しくなりました。
特に、Web開発にずっと取り組んでいたので、Webに関係のあるライブラリをどんどん作って、Web開発にもNimが参入するようになったら嬉しいなと思います。

開発は主にメタプログラミングが中心で、いつもと考えるステップが(このコードで出力されるコードが〇〇と働くから...のように)一段増えたので混乱することも多くあり、コードも綺麗にはかけませんでしたが、できる限り直していって、正式リリースを目指したいと思います。

駄文ではありましたが、読んでいただきありがとうございました。
本ライブラリは

GitHub

で公開していますので、よければご覧ください。では!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでプルダウンリストを表示させる

先日、Railsで作成しているポートフォリオにJavaScriptを使い非同期通信で表示されるプルダウンリストを実装しました。

簡単なJavaScriptの記述を投稿いたします。
(HTML、CSSの記述など詳細はブログにも投稿しました。)
https://mikujo.hatenablog.com/entry/2020/10/17/220545

実装内容

今回の実装内容は2点です。

  • 親要素上にマウスを乗せると色が変わる(マウスを離すと消える)
  • 親要素をクリックすると子要素のプルダウンリストが表示される(リスト表示時にクリックするとリストが消える)

pulldown.jsの記述

今回は、app/javascriptリポジトリ内にpulldown.jsを作成しました。
こちらを記述します。

全体の関数を定義

まず、ページロード時に発火する関数を定義します。

function pullDown(){}
window.addEventListener('load', pullDown)

HTMLのIDを取得し、変数に代入

HTMLに設定したIDは以下です。

  • 親要素: lists
  • 子要素: pull-down

こちらを先ほど定義した関数内で変数に代入します

function pullDown(){
  const pullDownButton = document.getElementById("lists")
  const pullDownContent = document.getElementById("pull-down")
}

文字の色変更の関数を定義

取得したID(lists)の要素にマウスが乗った際のイベントを関数に定義します。

  pullDownButton.addEventListener('mouseover', function(){
    this.setAttribute("style", "color:#FFBEDA;")
  })

上記の記述では、マウスが乗った際にcolor:#FFBEDA;setAttributeによって適用されています。
また、マウスが離れた時のイベントも定義します。

  pullDownButton.addEventListener('mouseout', function(){
    this.removeAttribute("style", "color:#FFBEDA;")
  })

上記の記述では、マウスが乗った際にcolor:#FFBEDA;removeAttributeによって除去されています。

クリック時の動作の関数を定義

続いて要素クリック時のイベントを関数に定義します。

  pullDownButton.addEventListener('click', function(){
    if (pullDownContent.getAttribute("style") == "display:block;") {
      pullDownContent.removeAttribute("style", "display:block;")
    } else {
      pullDownContent.setAttribute("style", "display:block;")
    }
  })

上記の記述ではif文を用いて、display:block;が適用されているかどうかで発火するイベントを変えております。
display:block;が適用されている際は、removeAttribute で除去、
適用されていない際はsetAttributeで適用するというように記述しております。

全体像

以上でJavaScriptでプルダウンリストを表示させる記述が完成しました。
最後にJSの全体像になります。

function pullDown(){

  const pullDownButton = document.getElementById("lists")
  const pullDownContent = document.getElementById("pull-down")

  pullDownButton.addEventListener('mouseover', function(){
    this.setAttribute("style", "color:#FFBEDA;")
  })
  pullDownButton.addEventListener('mouseout', function(){
    this.removeAttribute("style", "color:#FFBEDA;")
  })

  pullDownButton.addEventListener('click', function(){
    if (pullDownContent.getAttribute("style") == "display:block;") {
      pullDownContent.removeAttribute("style", "display:block;")
    } else {
      pullDownContent.setAttribute("style", "display:block;")
    }
  })
}

window.addEventListener('load', pullDown)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

全自動ルービックキューブ(大嘘)つくってみた

前身:https://qiita.com/kob58im/items/c86f09f57a153bf34d05

See the Pen AutoCube_ThreeJs by kob58im (@kob58im) on CodePen.

応用元:https://qiita.com/kob58im/items/a35a0ae753a3093d21ba

ちょっとだけ説明(備忘録)

image.png
回転させたいboxを9個選んで回転させる。

回転処理(抜粋)
  mat.makeRotationFromEuler(new THREE.Euler( Math.PI*degX/180, 0, 0, 'XYZ' ));
  for(let y=0;y<3;y++){
    for(let z=0;z<3;z++){
      boxes[ほげほげ].geometry.applyMatrix(mat);
    }
  }

ただし、回転させると対象のboxの位置が変わってしまうので、配列(本プログラム上の変数boxIndexConversion)を使って、初期位置のboxが今3x3x3の立方体のどこにいるのかを把握する。(下記で変換している。)

位置情報の追従管理(Y軸抜粋)
  if (rotDirId==="Y"){
    swapBoxIndex(9*rotPIndex, 0, 6, 8, 2);
    swapBoxIndex(9*rotPIndex, 1, 3, 7, 5);
  }
  else if(rotDirId==="y"){
    swapBoxIndex(9*rotPIndex, 2, 8, 6, 0);
    swapBoxIndex(9*rotPIndex, 5, 7, 3, 1);
  }

中略

function swapBoxIndex(b, i0,i1,i2,i3) {
  let t = boxIndexConversion[b+i0];
  boxIndexConversion[b+i0] = boxIndexConversion[b+i1];
  boxIndexConversion[b+i1] = boxIndexConversion[b+i2];
  boxIndexConversion[b+i2] = boxIndexConversion[b+i3];
  boxIndexConversion[b+i3] = t;
}

そんな感じ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オープンデータを使って住所検索ライブラリを作りました

オープンデータな住所データを使用して文字列と緯度経度で住所検索できるNode.jsのモジュールを作りました。

japan-address-search
https://github.com/uedayou/japan-address-search

従来、住所検索を行う場合、GoogleやYahooなど外部のWebサービス・Web APIと連携して検索するものが多いと思います。少数のデータを検索する場合は特に問題ないですが、大量のデータを検索したい場合には外部サービスの制限やレスポンスの問題など利用するには不向きです。

このモジュールは、モジュール内のデータについて検索するので、大量のデータでも外部サービスに影響なくローカルで検索することができます。

このモジュールは、経済産業省のIMIコンポーネントツールを元に作成していますので、住所表記の正規化も行えます。

npmリポジトリにも公開しているので、npmコマンドで手軽にインストールできます。

https://www.npmjs.com/package/japan-address-search

インストール方法

コマンドラインで使う場合は npm でグローバルにインストールしてください。

$ npm install -g japan-address-search

Node.js のコード内で利用したい場合はローカルにインストールします。

$ npm install japan-address-search

使い方

コマンドラインで、文字列検索したい場合は以下のように実行します。

$ japan-address-search -s 神保町

緯度経度による逆ジオコーディング検索は以下のようになります。

$ japan-address-search --lat 35.675551 --lng 139.750413

どちらもデフォルトで最大10件表示されます。任意の件数を得たい場合はパラメータに--limit 5のように追加してください。

コード内での利用は
https://github.com/uedayou/japan-address-search#readme
を参照してください。

出力結果

検索結果は、JSONで以下のように出力されます。
町・丁目レベルには代表点として緯度経度が得られます。

{
  "@context": "https://imi.go.jp/ns/core/context.jsonld",
  "場所": [
    {
      "@type": "場所型",
      "住所": [
        {
          "@type": "住所型",
          "表記": "神保町",
          "都道府県": "千葉県",
          "都道府県コード": "http://data.e-stat.go.jp/lod/sac/C12000",
          "市区町村": "船橋市",
          "市区町村コード": "http://data.e-stat.go.jp/lod/sac/C12204",
          "町名": "神保町",
          "種別": "位置参照情報"
        }
      ],
      "地理座標": {
        "@type": "座標型",
        "緯度": "35.762159",
        "経度": "140.051552"
      }
    },
    {
      "@type": "場所型",
      "住所": [
        {
          "@type": "住所型",
          "表記": "神保町",
          "都道府県": "新潟県",
          "都道府県コード": "http://data.e-stat.go.jp/lod/sac/C15000",
          "市区町村": "見附市",
          "市区町村コード": "http://data.e-stat.go.jp/lod/sac/C15211",
          "町名": "神保町",
          "種別": "位置参照情報"
        }
      ],
      "地理座標": {
        "@type": "座標型",
        "緯度": "37.500056",
        "経度": "138.970828"
      }
    },
    {
      "@type": "場所型",
      "住所": [
        {
          "@type": "住所型",
          "表記": "神保町",
          "都道府県": "千葉県",
          "都道府県コード": "http://data.e-stat.go.jp/lod/sac/C12000",
          "市区町村": "船橋市",
          "市区町村コード": "http://data.e-stat.go.jp/lod/sac/C12204",
          "町名": "大神保町",
          "種別": "位置参照情報"
        }
      ],
      "地理座標": {
        "@type": "座標型",
        "緯度": "35.773812",
        "経度": "140.061923"
      }
    },
    {
      "@type": "場所型",
      "住所": [
        {
          "@type": "住所型",
          "表記": "神保町",
          "都道府県": "東京都",
          "都道府県コード": "http://data.e-stat.go.jp/lod/sac/C13000",
          "市区町村": "千代田区",
          "市区町村コード": "http://data.e-stat.go.jp/lod/sac/C13101",
          "町名": "神田神保町",
          "種別": "位置参照情報"
        }
      ]
    }
  ]
}

その他の機能

japan-address-searchモジュールには、現在の住所以外に明治時代以前の地名についても、文字列と緯度経度で検索することができます。コマンドラインでは、--oldをつけると検索対象が明治時代以前の地名になります。

$ japan-address-search -s 神保町 --old
$ japan-address-search --lat 35.675551 --lng 139.750413 --old

japan-address-search について

japan-address-searchIMIコンポーネントツールを改修して作成したものです。改修の経緯は以下のQiitaの記事にまとめています。

  1. IMI住所変換コンポーネントでいろんな住所を正規化してみた
  2. IMI住所変換コンポーネントを改造してリバースジオコーディングに対応してみた
  3. IMI住所変換コンポーネントを魔改造して昔の地名を検索できるようにしてみた

また、オープンデータとして

を利用しています。
各ツール・データを公開していただいている皆様には深く感謝いたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

地図タイルにおけるピクセルの大きさをメートルに変換する

1. 概要

地図タイルにおける1ピクセルの大きさ(メートル)を計算したい。

地球は平面ではないため、ピクセルの大きさは位置に依存することになる。したがって、タイル座標が与えられた時、当該タイルにおける1ピクセルの大きさを計算できるようにしたい。

2. 計算式

Understanding Scale and Resolution - Bing Maps Articles によると、以下のように緯度とズームレベルに依存した式で計算することができるらしい。

Map resolution = 156543.04 meters/pixel * cos(latitude) / (2 ^ zoomlevel)

この式をタイル座標とピクセル座標の関係、ピクセル座標と緯度経度の関係と組み合わせれば、目的を達成することができる。

3. 実装 (JavaScript)

3.1. 緯度・ズームレベルからピクセルサイズを計算

計算式通り実装する。latitudeの単位が[rad]であることに注意。

// Calculate resolution [meter/pixel]
const resolution = (latitude, zoom) => 156543.04 * Math.cos(latitude) / (1 << zoom)

3.2. タイル座標から緯度経度を計算

ここでは、タイル座標 tileZ / tileX / tileY のタイル上の (pX, pY) 要素の緯度経度を計算する関数を実装している。

// Calculate lat/lon of pixel (pX, pY) on tile tileZ/tileX/tileY
function pixelOnTileToLatLon(pX, pY, tileZ, tileX, tileY) {
  const L = 85.05112878;
  // Pixel coordinate
  const x = 256 * tileX + pX;
  const y = 256 * tileY + pY;

  const lon = 180 * (x / (1 << (tileZ + 7)) - 1);
  const lat = (180/Math.PI) * Math.asin(Math.tanh(
    - Math.PI / (1 << (tileZ + 7)) * y + Math.atanh(Math.sin(L * Math.PI/180))
  ));
  return {lat: lat, lon: lon};
}

3.3. タイル座標からピクセルサイズを計算

上記の関数を組み合わせ、タイル座標で指定されたタイルにおけるピクセルサイズを計算する関数を実装する。
同じタイル内でも緯度によって結果が異なるため、与えられたタイル座標について北西 / 南東 / 中心の緯度経度を計算し、それぞれについて計算する。

// Calculate pixel size [m] of given tile
function pixelSize(tileZ, tileX, tileY) {
  // North west / South east / Center
  const pNW =pixelOnTileToLatLon(0, 0, tileZ, tileX, tileY);
  const pSE = pixelOnTileToLatLon(255, 255, tileZ, tileX, tileY);
  const pCenter = pixelOnTileToLatLon(128, 128, tileZ, tileX, tileY);

  const deg2rad = deg => deg / 180 * Math.PI;

  return {
    min: resolution(deg2rad(pNW.lat), tileZ),
    max: resolution(deg2rad(pSE.lat), tileZ),
    center: resolution(deg2rad(pCenter.lat), tileZ)
  };
}

4. 例

例えばタイル座標 13/7262/3232 (鍋割山付近)についてpixelSize(13, 7262, 3232)を計算すると、1ピクセルあたり15.56 - 15.57 [m] であることが分かる。

{min: 15.564756939473298, max: 15.57165416877383, center: 15.568219450043276}

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】外部JSファイルをmoduleとして読み込む方法【ES6】

scriptタグにtype="module"を忘れずに

index.html
<script type="module" src="js/main.js"></script>

webpackではないので拡張子の「.js」は必要

main.js
import { incidentDatalist } from './module.js'
console.log(incidentDatalist);

今回は変数としてexportする

module.js
export let incidentDatalist = [
  { year: 1192, name: '鎌倉幕府が開かれる'},
  { year: 1600, name: '関ヶ原の戦い'},
  { year: 794, name: '平安京へ遷都'},
  { year: 1923, name: '関東大震災が起きる'},
]

console.log(incidentDataList);などで外部JSの読み込みができたことを確認できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワイ「なに!?ライブラリをラップするやと!?」

とあるシステム会社にて

ワイ「なあ、ハスケル子ちゃん」
ワイ「いま開発してるWebアプリケーションについて、相談したいことがあるんやけど」

ハスケル子「はい」
ハスケル子「どんな内容ですか?」

ワイ「あのな」
ワイ「色んなページで、配列をソートして表示する必要があるんやけど」
ワイ「そのソート内容がちょっと複雑やねん」
ワイ「なんかオススメのライブラリある?」

ハスケル子「LodashのorderByなんて便利ですよ」

ワイ「マジかいな」
ワイ「さっそく使ってみるわ」

ライブラリを導入してみる

ワイ「まずは・・・」

ターミナル
npm install lodash.orderby

ワイ「↑こう、npmでインストールして」

JavaScript
import lodashOrderBy from 'lodash.orderby'

ワイ「↑こう、importしてやれば使えるな」
ワイ「さて、このライブラリはどんな風に使うんかな?」

ハスケル子「ええと、例えば・・・」

  • ユーザーの情報が格納されたuserListという配列をソートする
  • 名前の昇順でソートする
  • 同姓同名の場合は、年齢の降順でソートする

ハスケル子「・・・なんてことがしたい場合は」

JavaScript
const sortedUserList = lodashOrderBy(userList, ['name', 'age'], ['asc', 'desc'])

ハスケル子「↑こんな風に書きます」

ワイ「なるほどな」
ワイ「第一引数には、ソートしたい配列を」
ワイ「第二引数には、ソートするプロパティ名を」
ワイ「第三引数には、昇順 or 降順を」
ワイ「それぞれ指定すればええんやな!」

ハスケル子「はい」

ワイ「よっしゃ、この関数、いろんなページで使わしてもらうわ!」

ハスケル子「待ってください、やめ太郎さん」

ワイ「ん、なに?」

ハスケル子「lodash.orderbyラッパー関数を作って」
ハスケル子「各ページからは、そのラッパー関数を呼び出すようにした方がいいですよ」

ワイ「ラッパー・・・?」
ワイ「何なんそれ」

ハスケル子「lodash.orderbyラップした関数を、自分で書くってことですよ」

ワイ「え、なんで?」

ハスケル子「その方が、保守性が上がります」
ハスケル子「具体的には、将来的にそのライブラリを別のものに入れ替える場合とかに」
ハスケル子「だいぶ手間が省けます」

ワイ「(何を言うてるか分からんけど・・・)」
ワイ「よっしゃ、分かった!やってみるわ!

ライブラリをラップする、とは

ワイ「何をすればええねや・・・?」
ワイ「ラッパー関数を書けって言うてたな」
ワイ「・・・ということは・・・」

JavaScript
// lodash.orderby を import
import lodashOrderBy from 'lodash.orderby'

// lodash.orderby をラップした関数
export const orderBy = (itemsList, sortKeys, sortOrders) => {
  return lodashOrderBy(itemsList, sortKeys, sortOrders)
} 

ワイ「↑こんな感じか・・・?」
ワイ「いや、意味なさすぎるやろ」
ワイ「だって、このorderBy関数」
ワイ「受け取った引数をlodashOrderByにそのまま渡してるだけやもん」
ワイ「これは多分ちがうな」
ワイ「ほなラッパー関数って、何のことやろ・・・」
ワイ「ラッパー、ラッパー・・・」

ワイ「・・・あっ!!」

分かった

ワイ「ハスケル子ちゃん、さっそくライブラリをラップしてみたで!」
ワイ「聞いてみてくれや!」

ハスケル子「は、はい」
ハスケル子「(聞くとは・・・?)」

ワイ「行くで!」

誰かが作ったライブラリ
ラップするだけでだいぶマシ
入れ替える手間ほぼ皆無だし
マジ教えてくれたアイツ神

ワイ「どや?」

ハスケル子「逆に、どう思います?
ハスケル子「これをどうプログラムに組み込むんですか?」

ワイ「そ、それな・・・」

ハスケル子「そうじゃなくて」
ハスケル子「最初に書いてた関数の方で合ってますよ」

ワイ「ファッ!?」

JavaScript
// lodash.orderby をラップした関数
export const orderBy = (itemsList, sortKeys, sortOrders) => {
  return lodashOrderBy(itemsList, sortKeys, sortOrders)
} 

ワイ「↑これかいな!?」
ワイ「なんでこんな、意味のないorderBy関数を作るん?」

ハスケル子「意味ありますよ」
ハスケル子「例えば、ライブラリをラップしないで」
ハスケル子「いろんなページから直接lodashOrderByを呼び出している場合だと」
ハスケル子「将来的にライブラリを入れ替える場合とかに面倒じゃないですか」

ワイ「まあ、ちょっとだけ面倒やな」
ワイ「いろんなファイルのlodashOrderByを」
ワイ「xxxSortLibraryとかに検索置換せなあかんもんな」
ワイ「でも大した手間やないやん?」

ハスケル子「検索置換だけじゃ済まない場合もありますよ」
ハスケル子「例えば・・・」

JavaScript
// 新しく使う xxxSortLibrary というライブラリは
// 引数の渡し方が lodash.orderby と違う
// (オブジェクトとして渡さないといけない)
const sortedUserList = xxxSortLibrary({
  array: userList,
  sortKeys: ['name', 'age'],
  sortOrders: ['asc', 'desc']
})

ハスケル子「↑こんな感じで」
ハスケル子「似たようなライブラリでも、引数の渡し方が結構ちがうこともあるじゃないですか」

ワイ「ほんまや・・・」
ワイ「こんなん、いろんなページで修正するとなったら、結構めんどくさいで・・・」

ハスケル子「ですよね?」

ラッパー関数を作っていた場合

ハスケル子「ラッパー関数を作って、それを使っていた場合は」
ハスケル子「1箇所だけの修正で済みます」

JavaScript
// xxxSortLibrary を import
import xxxSortLibrary from 'xxxSortLibrary'

// xxxSortLibrary をラップした関数
export const orderBy = (itemsList, sortKeys, sortOrders) => {
  return xxxSortLibrary({
    array: userList,
    sortKeys,
    sortOrders
  })
} 

ハスケル子「↑こんな感じで、ラッパー関数を修正してあげるだけです」

ワイ「なるほどなぁ」
ワイ「自作のラッパー関数が受け取った引数たちを」
ワイ「今まではそのまま渡すだけやったけど」
ワイ「新しいライブラリ関数に合う形式に変換して、渡してやればええわけか」

ハスケル子「そうです」
ハスケル子「ラッパー関数が作ってあれば」
ハスケル子「ライブラリを入れ替えた場合でも」
ハスケル子「各ページからの呼び出し方は変わりません」

JavaScript
// ラッパー関数の引数は変わっていないので
// 各ページからは今まで通り使える
const sortedUserList = orderBy(userList, ['name', 'age'], ['asc', 'desc'])

ワイ「おお、ほんまや・・・」
ワイ「呼び出し側はそのままでOKなんやね」

ハスケル子「はい」
ハスケル子「それに、ライブラリを入れ替える場合だけじゃなく」
ハスケル子「ライブラリのバージョンアップによって、インターフェース(使い方)が変わる可能性もありますし」

ワイ「なるほどな」
ワイ「最近のフロントエンド開発では、多数のnpmライブラリを使うし」
ワイ「流行り廃りが激しいから、ライブラリを入れ替えることもあるもんな」
ワイ「メンテナンスコストのことも考えとかんとな」

まとめ

  • ライブラリの関数をラップした関数を作っておくと、あとあと便利
  • 逆に、ラップしないで直接呼び出していると、ライブラリの入れ替え時に手間がかかる
  • 新旧ライブラリのインターフェースの違い(使い方の違い)を、ラッパー関数が吸収してくれる
  • 結果的に保守性が上がる
  • 関数名をiLoveHipHop()にするとか、そういうことではない

ワイ「ってことやな!」

ハスケル子「ですね!」

〜おしまい〜

スペシャルサンクス

ばりとんさん(考え方を教えてもらったから)
きんみさん(ラップをパクらせてもらったから)

追記

ばりとんさんも記事を書いてたから、被りまくってしまった。
ばりとんさん、申し訳ありません・・・!

わて「なにっ!?韻を踏むだけで保守性があがるやと!?」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptで同じクラスを持つ複数の入力値を取得する

Javascriptを使って、任意のHTML要素にアクセスする方法です。

同じクラスのinput(入力値)を取得

例えば、このようなコンタクトフォームがあるとします。

index.html
<h2>お問い合わせフォーム</h2>
<form class="contactForm" action="" method="post">
<table>
 <tr>
  <th><label>お名前</label></th>
  <td><input class="item" type="text" name="name" value="タンジロウ"></td>
 </tr>
 <tr>
  <th><label>メールアドレス</label></th>
  <td><input class="item" type="email" name="email" value="kisatsu@gmail.com"></td>
 </tr>
 <tr>
  <th><label>電話番号</label></th>
  <td><input class="item" type="tel" name="tel" value="090-1234-5678"></td>
 </tr>
 <tr>
  <th><label>お問い合わせ内容</label></th>
  <td><textarea class="item" name="message">お問い合わせ内容テキスト</textarea></td>
 </tr>
 <tr>
  <td><input type="submit" id="submitBtn" name="btn_confirm" value="送信"></td>
 </tr>
 </form>
</table>

全ての入力値に適切な値が入っているかを確認する為に、同じクラスitemをキーに要素を取得します。

getElementsByClassName

getElementsByClassNameは同じクラス名を検索して、返り値に配列風のHTMLCollectionを返します。elementにsがついていることに注意してください。

main.js
const ItemList = document.getElementsByClassName("item"); 

for(let i = 0; i < ItemList.length; i++) {
    console.log(ItemList.item(i).value);
}

コンソールの出力結果

コンソールの出力結果です。Itemにアクセスすることで好きな値をチェックすることができます。
mojikyo45_640-2.gif
クラス名を複数指定すると、and指定となり両方のクラスを持つ要素を取得できます。

document.getElementsByClassName("item email")

HTMLCollectionを配列に変換

配列に変換することで、配列のメソッドが使えるようにすることもできます。用途によって、使い分けましょう。

main.js
const ItemArray = Array.prototype.slice.call(ItemList);

ItemArray.forEach(function(array) {
    console.log(array);
});

コンソールの出力結果

mojikyo45_640-2.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数オブジェクトからなる配列を、要素内のプロパティ値を基準にソート

Array.prototype.sort()について

Javascript初心者です。
paizaで練習問題を特訓していましたが、以下のような配列をsortするのに
一苦労したので備忘録として残しておきます。
間違いや拙い点などあるかと存じますが、何卒ご了承ください。

// 配列の中のオブジェクトの中のプロパティ値を基準に並び変えたい
// これを
[
  { name: 'cat', power: 50 },
  { name: 'dog', power: 100 },
  { name: 'mosquito', power: 1 },
  { name: 'lion', power: 2000 },
]

// こうしたい
[
  { name: 'lion', power: 2000 },
  { name: 'dog', power: 100 },
  { name: 'cat', power: 50 },
  { name: 'mosquito', power: 1 },
]

その前にsort()の基本形

上記の例とは別で、配列の中にそのまま何らかの値が列挙されている場合の使い方。

辞書順に並べる

const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months);
// expected output: Array ["Dec", "Feb", "Jan", "March"]

名前の辞書順に並べたいだけならば、このように引数無しのsort()でOK。
だけど、ほとんどの場合は数値を基準に並べたい...

数値の昇順・降順で並べる

昇順
const months = [3, 1, 2, 12];

months.sort((a,b) => {
    return a - b;
});

console.log(months);
// expected output: Array [1,2,3,12]
降順
const months = [3, 1, 2, 12];

months.sort((a,b) => {
    return b - a;
});

console.log(months);
// expected output: Array [12,3,2,1]

sortの引数に比較関数を渡します。
a,bにはそれぞれ配列の中の数値が入っています。

returnが正の値 : 第1引数を第2引数の後ろに並べ変える
returnが負の値 : 第1引数を第2引数の前に並べ替える
returnが0 : 並べ変えを行わない

(となっていますが、正直よくわからないので機械的に覚えちゃってます...)

引用
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

本題

let creatures = [
  { name: 'cat', power: 50 },
  { name: 'dog', power: 100 },
  { name: 'mosquito', power: 1 },
  { name: 'lion', power: 2000 },
]

// a,bにはオブジェクトが入っているので、そのプロパティ名を指定して比較すれば並べ替え可能。
creatures.sort((a,b) => {
    return b.power - a.power; 
}); 

console.log(creatures);
// expected output:
//[
//  { name: 'lion', power: 2000 },
//  { name: 'dog', power: 100 },
//  { name: 'cat', power: 50 },
//  { name: 'mosquito', power: 1 },
//]

ひとこと

配列ループ系の関数は全部知っておいた方が良さそうです...
(forEach,map,filter ...etc)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

参考サイトとして表示するurlを形式ごと取得する

初書:2020/10/25

前書き

QiitaのMarkdownを書く際に、参考サイトとしてurlをコピーして貼り付け・・・をするわけだが、
それをJavascriptで形式ごと([タイトル](url))取得するコードを作成する。
元々これを書いてくれていたページがあって、LGTMしていたはずなのだが、知らぬ間に消えていたので改めて自分でコードを作ることにした。

前提

mac版safari:14.0
mac版chrome:86.0
の二つで動作確認。それ以外(特にIE)は知らない

タイトル・urlを取得する

これは調べたらいくらでも出てくる。

    var str = "["+document.title+"]("+location.href+")";
    alert(str);

これで、例えばこのページなら[参考サイトとして表示するurlを形式ごと取得する - Qiita](https://qiita.com/yuu_1st/items/11c2ca89276962b2e488)が表示されるはず

一応これで完成なのだが、せっかくなのでもう少し便利にしたい。

取得したものをクリップボードにコピーする

調べてみると、以下のサイトに出会った。
[JavaScript]クリップボードを使ったコピーとペースト - Qiita

ClipboardEvent.clipboardDataAsync Clipboard APIがあったのだが、後者の方が簡単に記述できるので、今回はこちらを使用する。
対応状況をみると、IE以外は対応しているみたい? Clipboard API - Web APIs | MDN

ということで、コードを記述してみる

if(navigator.clipboard){
    navigator.clipboard.writeText("["+document.title+"]("+location.href+")")
        .then(()=>alert("コピーしました"))
        .catch((e)=>console.error("コピーできませんでした"));
}else{
    alert("コピーできませんでした。");
}

一応alertでコピーできたかのエラーを吐くようにした。
writeTextは非同期処理なので、thenとcatchを使用している。仕様がわからない人はjavascript promiseで検索

・・・せっかくなのでもっと便利にしよう。

ブックマークに追加する

ブックマークに追加すれば、ボタン一つでクリップボードにコピーすることが出来る。ctrl+cは一切使わない。
一応上記を独自minifyしたコードを下に置いておく。

if(navigator.clipboard)navigator.clipboard.writeText("["+document.title+"]("+location.href+")").then(()=>alert("コピーしました")).catch(()=>console.error("コピーできませんでした"));else alert("コピーできませんでした。");

これをコピーして、先頭にjavascript:を追加してブックマークすれば、完成。
(ブックマークへの追加の仕方の説明は要らないよね。。。?)

終わりに

上のコードをChromeのアドレスバーに入れると、「アクティブウィンドウがありません」というエラーを吐くので、とりあえず試してみようって感じで入れる場合は注意。

括弧の順番いつも迷うので、こういうのを保存しておくと便利かも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 組み込みオブジェクト まとめ

よく使いそうな標準データ型を扱うオブジェクトをまとめます。他の、構文などは今回はまとめません。

文字列操作 Stringオブジェクト

メソッド 概要
slice(A,B) 文字列からA+1 ~ B文字目を抽出
split(str,[lim]) strの文字で文字列を分割し、その結果を配列として取得、limは最大分割数
toLowerCase() 小文字に変換
toUpperCase() 大文字に変換
concat(str) 文字列の後方にstrを連結
trim() 文字列の前後から空白を削除
length 文字列の長さを取得

数値を操作する Numberオブジェクト

無限大やNumberで表せる最大値などのプロパティもある。今回はメソッドのみまとめる。

メソッド 概要
toFixed(dec) 小数点第dec位になるように四捨五入
toPrecision(dec) 指定桁数になるように変換(足りない場合は0で補足)
parseFloat(str) 文字列を小数点数に変換
parseInt(str) 文字列を整数に変換

基本的な数学演算の実行 Mathオブジェクト

メソッド 概要
abs(num) 絶対値
max(A,B,...), min A,B,..の中での最大値(最小値)
random() 0~1未満の乱数
PI π
三角関数(num) 三角関数系

配列操作 Arrayオブジェクト

メソッド 概要
length 配列のサイズ
toString() [要素,要素,..]の形式で文字列に変換
entries(),key(),value() 全てのキー/要素のそれぞれを取得
concat(ary) 指定配列を現在の配列に連結
slice(A,B) 配列ないのA〜B番目の要素の抜き出し
pop() 配列末尾の要素を取得し、削除
push(A,B,...) 配列末尾に要素を追加
shift() 配列先頭の要素そ取得し、削除
unshift(A,B,...) 配列先頭に指定要素を追加
reverse() 逆順に並び替え(反転)
sort() 要素を昇順に並び替え

日付・時刻 Dateオブジェクト

Dateオブジェクトは必ずnew でコンストラクターを経由する必要がある。
Dataオブジェクトの値を変更するには、setXxxxメソッドにすれば良い。

メソッド 概要
getFullYear() 年(4桁)
getMonth() 月(0〜11)
getDate() 日(1〜31)
getDay() 曜日(0:日曜〜6:土曜)
getHours() 時(0〜23)
getMinutes() 分(0〜59)
getSeconds() 秒(0〜59)

日付などの加算・減算については値を超えたとしても先月に戻るなど自動でよしなにしてくれる

よく使う機能 Globalオブジェクト

メソッド 概要
isFinite(num) 有限値かどうか
isNan(num) 数値でないかどうか(Not a Number)
Number(val),String(val) 数値形、文字列形に変換
parseFloat(str),parseInt(str) 文字列を不動小数点・整数値に変換
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Google Maps APIによるGoogle Mapの表示と複数地点間のルート検索

はじめに

参考になる記事がとても少なく、ポートフォリオ作成で一番苦戦した部分なので、自分の学習のためアウトプットとして残す、とともにだれかの役に立てればいいなと思ったので書きました!
初学者なりにこの記述の意味はなんだ?と思う部分はしっかり説明したつもりです。
当たり前だろ!と思う部分も多々あると思いますがご了承ください。

目標

Google Maps APIでGoogle Mapを表示させるとともに、マーカーの吹き出しから任意にルート検索リストに追加でき、複数地点のルートを検索する機能を目標とします。
ezgif com-optimize-2

開発環境

・Ruby: 2.5.1
・Rails: 5.2.1
・OS: macOS

前提

・Slimの導入
・公式のGoogle Maps Platformで以下のAPIの有効化
 ・Maps JavaScript API
  → GoogleMapの表示
 ・Geocoding API
  → 住所から緯度経度の算出
 ・Directions API
  → ルート検索

設定

1. 必要なgemをインストール

Gemfile
gem 'dotenv-rails' # APIキーを環境変数化
gem 'gon' # コントローラーで定義したインスタンス変数をJavaScript内で使用出来るようにする。
gem 'geocoder' # 住所から緯度経度を算出する。
ターミナル
$ bundle install

2. APIキーを環境変数化

アプリケーション直下に「.env」ファイルを作成

ターミナル
$ touch .env 

自身のAPIキーを' 'の中に記述

.env
GOOGLE_MAP_API = '自身のコピーしたAPIキー'
.gitignore
/.env

3. turbolinksの無効化

Gemfile
gem 'turbolinks' # この行を削除
app/assets/javascripts/application.js
//= require turbolinks // この行を削除

data-turbolinks-track':'reload'属性の削除

app/views/layouts/application.html.slim
= stylesheet_link_tag    'application', media: 'all'
= javascript_include_tag 'application'

4. Geocoding APIを使用できるようにする

geocorderの設定ファイルを作成し、編集

ターミナル
$ touch config/initializers/geocoder.rb
config/initializers/geocoder.rb
# 追記
Geocoder.configure(
  lookup: :google,
  api_key: ENV['GOOGLE_MAP_API']
)

これで設定は終了です。ここからGoogleMapを表示していく実装に入ります。

GoogleMapの表示

1. 追加したいモデルにカラムを追加

自分のアプリの場合はPlaceモデルにaddressカラムを追加します。
latitude, longitudeカラムはGeocoding APIによってaddressカラムの値から算出された経度・緯度の値です。小数の値なので型はfloatを使います。

ターミナル
$ rails g migration AddColumnsToPlaces address:string latitude:float longitude:float
ターミナル
$ rails db:migrate

2. モデルを編集

models/place.rb
  # 追記
  geocoded_by :address # addressカラムを基準に緯度経度を算出する。
  after_validation :geocode # 住所変更時に緯度経度も変更する。

3. コントローラーを編集

controllers/places_controller.rb
def index
  @place = Place.all
  gon.place = @place # 追記
end

private
  def place_params
    # ストロングパラメーターに「address」を追加
    params.require(:place).permit(:name, :description, :image, :address)
  end

4. ビューを編集

①application.html.slimを編集
CSSとJavaScriptより先に、gonを読み込むよう記述します。

views/layouts/application.html.slim
doctype html
html
  head
    title
      | app_name
    = csrf_meta_tags
    = csp_meta_tag
    = include_gon # 追記
    = stylesheet_link_tag    'application', media: 'all'
    = javascript_include_tag 'application'

②新規登録画面に住所入力フォームを追加

views/places/new.html.slim
= f.label :address, '住所'
= f.text_field :address, class: 'form-control'

③GoogleMapを表示するファイルに記述

views/places/index.html.slim
div id = 'map_index' # idを付与, この部分にjsファイルで記述したGoogle Mapが埋め込まれる
- google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe
script{ async src = google_api }

.map-route
  < ルート検索リスト >
  ul id = "route-list" class = "list-group" # jsファイルで吹き出しの追加ボタンによってその場所がli要素に追加される


div id = 'directions-panel' # 距離・時間が埋め込まれる
  < 各地点間の距離・時間 >
  ul id = "display-list" class = "display-group"

.map-search
   = button_tag "ルート検索", id: "btn-search", class: "btn btn-primary", onclick:     "search()" # クリック処理でsearch()関数を呼び出す

[ google_api = 〜〜〜〜の部分について ]
→ callback処理で読み込み時にinitMap関数を呼び出す。
→ .html_safeはエスケープ処理
→ async属性によって非同期でJavaScriptを読み込みレンダリングを早くする。

④GoogleMapで表示したいサイズをscssに記述

stylesheets/application.scss
#map_index{
  height: 400px;
  width: 400px; 
}

5. JavaScriptのファイルを編集

ここが肝です。
assets/javascripts直下に新たなファイルを作成し、記述します。
だいぶ長く見にくいかと思いますが、変数を定義したのち、関数の定義をそれぞれ行っているだけです。
関数は、
・initMap
・markerEvent( i )
・addPlace(name, lat, lng, number)
・search()
の順で4つがあります。
わかりにくい部分やポイントは、コメントアウトで説明していますので参考にしてください。

assets/javascripts/googlemap.js
var map
var geocoder
var marker = [];
var infoWindow = [];
var markerData = gon.places; // コントローラーで定義したインスタンス変数を変数に代入
var place_name = [];
var place_lat = [];
var place_lng = [];

// GoogleMapを表示する関数(callback処理で呼び出される)
function initMap(){
    geocoder = new google.maps.Geocoder()
    // ビューのid='map_index'の部分にGoogleMapを埋め込む
    map = new google.maps.Map(document.getElementById('map_index'), {
      center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心
      zoom: 9,
    });

    // 繰り返し処理でマーカーと吹き出しを複数表示させる
    for (var i = 0; i < markerData.length; i++) {
      // 各地点の緯度経度を算出
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i]['latitude'],
        lng: markerData[i]['longitude']
      });

      // マーカーの表示
      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      // 吹き出しの表示
      let id = markerData[i]['id']
      place_name[i]= markerData[i]['name'];
      place_lat[i]= markerData[i]['latitude'];
      place_lng[i]= markerData[i]['longitude'];
      infoWindow[i] = new google.maps.InfoWindow({
        // 吹き出しの中身, 引数で各属性の配列と配列番号を渡す
        content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`
      });
      markerEvent(i);
    }
  }
}

// マーカーをクリックしたら吹き出しを表示
function markerEvent(i) {
  marker[i].addListener('click', function () {
    infoWindow[i].open(map, marker[i]);
  });
}

// リストに追加する
function addPlace(name, lat, lng, number){
  var li = $('<li>', {
    text: name[number],
    "class": "list-group-item"
  });
  li.attr("data-lat", lat[number]); // data-latという属性にlat[number]を入れる
  li.attr("data-lng", lng[number]); // data-lngという属性にlng[number]を入れる
  $('#route-list').append(li); // idがroute-listの要素の一番後ろにliを追加
}

// ルートを検索する
function search() {
  var points = $('#route-list li');

  // 2地点以上のとき
  if (points.length >= 2){
      var origin; // 開始地点
      var destination; // 終了地点
      var waypoints = []; // 経由地点

      // origin, destination, waypointsを設定する
      for (var i = 0; i < points.length; i++) {
          points[i] = new google.maps.LatLng($(points[i]).attr("data-lat"), $(points[i]).attr("data-lng"));
          if (i == 0){
            origin = points[i];
          } else if (i == points.length-1){
            destination = points[i];
          } else {
            waypoints.push({ location: points[i], stopover: true });
          }
      }
      // リクエストの作成
      var request = {
        origin:      origin,
        destination: destination,
        waypoints: waypoints,
        travelMode:  google.maps.TravelMode.DRIVING
      };
      // ルートサービスのリクエスト
      new google.maps.DirectionsService().route(request, function(response, status) {
        if (status == google.maps.DirectionsStatus.OK) {
          new google.maps.DirectionsRenderer({
            map: map,
            suppressMarkers : true,
            polylineOptions: { // 描画される線についての設定
              strokeColor: '#00ffdd',
              strokeOpacity: 1,
              strokeWeight: 5
            }
          }).setDirections(response);//ライン描画部分

            // 距離、時間を表示する
            var data = response.routes[0].legs;
            for (var i = 0; i < data.length; i++) {
                // 距離
                var li = $('<li>', {
                  text: data[i].distance.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);

                // 時間
                var li = $('<li>', {
                  text: data[i].duration.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);
            }
            const route = response.routes[0];
            // ビューのid='directions-panel'の部分に埋め込む
            const summaryPanel = document.getElementById("directions-panel");
            summaryPanel.innerHTML = "";

            // 各地点間の距離・時間を表示
            for (let i = 0; i < route.legs.length; i++) {
              const routeSegment = i + 1;
              summaryPanel.innerHTML +=
                "<b>Route Segment: " + routeSegment + "</b><br>";
              summaryPanel.innerHTML += route.legs[i].start_address + "<br>" + "" + "<br>";
              summaryPanel.innerHTML += route.legs[i].end_address + "<br>";
              summaryPanel.innerHTML += "<" + route.legs[i].distance.text + ",";
              summaryPanel.innerHTML += route.legs[i].duration.text + ">" + "<br>";
            }
        }
      });
  }
}



吹き出しの内容のcontent部分の補足:(データの受け渡しの方法で苦戦したので)

content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`

addPlace(place_name, place_lat, place_lng, ${i})
この関数の呼び出しでは前の3つの引数は配列として渡しています。4つ目の引数は配列の中でどの情報かを表すための番号(インデックスと呼びます。)を式展開したものです。JavaScriptでの式展開はこの形だそうです。
このような引数を用意することで、関数addPlace(name, lat, lng, number)は正常にどのデータであるかという情報を処理できるのです。

最後に

最後まで読んでくださり、ありがとうございます。
自分自身、現在ポートフォリオが完成に近づき就職活動を本格的に始め出したような状態です!
目標を持ってポートフォリオ作成、転職活動など行っている方を心から応援しています、共に頑張りましょう!!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

脳死でアップデートは危険!Vue3.0の破壊的変更点を要チェック!!

はじめに

Vue3.0がリリースされましたが、Vue2系からアップデートしましたか?
実は破壊的な変更点があり、脳死でアップデートすると危険です。
今回は破壊的変更点の中で特に影響が大きそうな部分を抽出して解説します!!

動画でも解説してるので、動画が好きな方はそちらを確認してみてください!!
【YouTube動画】 Vue3.0の破壊的変更点
Vue3.0の破壊的変更点

Global API

実際のコードの変更をみていきましょう!
以前は以下のように使っていた部分が

import Vue from 'vue'
import App from './App.vue'

Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

new Vue({
  render: h => h(App)
}).$mount('#app')

こうなります!

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.mount(App, '#app')

createAppでインスタンスを作成した後に、Vueの設定を追加していきます。
Vue.xxxxと書いていた部分は修正しないといけませんね。

Global API Tree Shaking

Tree Shakingは使われないコードを除去する仕組みです。
webpackではサポートされてますね。

以前のバージョンでは使わないメソッドもバンドルされていましたが、新しいバージョンでは明示的に書かないとバンドルされないようになりました。
これのおかげで、ファイルサイズがより小さくなります!

影響を受けるのは以下のAPIなので、使ってる方は修正が必要です。

Vue.nextTick
Vue.observable
Vue.version
Vue.compile
Vue.set
Vue.delete

修正するときはimportで使うAPIを指定します。

// Before
import Vue from 'vue'

Vue.nextTick(() => {
  // something DOM-related
})

// After
import { nextTick } from 'vue'

nextTick(() => {
  // something DOM-related
})

v-model

v-modelの書き方も変わりました。
例えば、以下のように書いていた場合

<Comp :value='pageTitle' @input='pageTitle=$event'/>

このように変更する必要があります。

<Comp :modelValue='pageTitle' @update:modelValue='pageTitle=$event'/>

Functional Component

Functional Componentを使っていた方は書き方が簡単になりました。
以下のようにhをインポートして使います。

import { h } from 'vue'

まとめ

いかがでしたか?
間違ってる部分やもっと解説して欲しい部分があれば、コメントいただけると嬉しいです!

また、twitteryoutubeでのコメントもお待ちしています!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】jQueryを使わずにスムーススクロール

他人の記事を引用しています。
今回は自分用の備忘録です。

main.js
const smoothScrollTrigger = document.querySelectorAll('a[href^="#"]');
  for (let i = 0; i < smoothScrollTrigger.length; i++){
    smoothScrollTrigger[i].addEventListener('click', (e) => {
      e.preventDefault();
      let href = smoothScrollTrigger[i].getAttribute('href');
       let targetElement = document.getElementById(href.replace('#', ''));
      const rect = targetElement.getBoundingClientRect().top;
      const offset = window.pageYOffset;
      const gap = 60;
      const target = rect + offset - gap;
      window.scrollTo({
        top: target,
        behavior: 'smooth',
      });
    });
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】動的なデータの反映まとめ

Vue.jsの十八番であるリアクティブなデータの反映の種類をまとめてみました

ディレクティブ等 紐付け先 どうしたいか 実例
v-model data 入力内容や選択内容を動的にしたい input(type="text", v-model="todo.message")
v-bind data htmlタグの属性を動的にしたい img(:src="todo.image)
{{ }} data 表示させる文字を動的にしたい p{{ todo.message }}
{{ }} computed dataの値を変化させたものを動的にしたい p{{ remaining }}

v-model

<template>
  <input type="text" v-model="todo.title">
</template>

<script>
let vm = new Vue({
  el: '#app',

  data: {
    todo: {
      { title: '' },
    }
  },
})
</script>

v-bind

<template>
  <img :src="todo.image">
</template>

<script>
let vm = new Vue({
  el: '#app',

  data: {
    newItem: '',
    todo: {
      { title: 'task1', image: "" },
    }
  },
})
</script>

{{ }}

dataオプションと連動

<template>
  <span>{{ todos[0].title }}</span>
</template>

<script>
let vm = new Vue({
  el: '#app',

  data: {
    todo: {
      { title: 'task1', isDone: false },
    }
  },
})
</script>

computedオプションと連動

<template>
  <span>{{ remaining }}</span>
</template>

<script>
let vm = new Vue({
  el: '#app',

  data: {
    todos: [
      { title: 'task1', isDone: true },
      { title: 'task2', isDone: false },
      { title: 'task3', isDone: false },
    ]
  },
  computed: {
    remaining: function(){
      let remainItems = this.todos.filter(function(todo){
        return !todo.isDone;
      });
      return remainItems.length;
    }
  }
})
</script>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】使用しているブラウザを判定する

使用しているブラウザを判定するにはJavaScriptNavigatorID.userAgentプロパティを使います。

ブラウザ情報の取得

var userAgent = window.navigator.userAgent;

これで、ブラウザの情報を取得できますが、ユーザーエージェントを使って取得できる情報は大文字・小文字が乱立している文字列なので、判定しやすくするために全て大文字、または全て小文字に統一します。そこで先ほどのソースに「toLowerCase()」を追記してこの文字列を扱いやすいように修正します。

ブラウザの識別子

以下の文字列で各ブラウザを識別できます。

ブラウザ 識別子
IE(11未満) MSIE
IE(11以上) Trident
旧Edge Edge
Edge(最新バージョン) Edg
Google Chrome Chrome
FireFox Firefox
Safari Safari

ブラウザ情報の判定

var userAgent = window.navigator.userAgent.toLowerCase();

indexOfを使って判定します。

if(userAgent.indexOf('msie') != -1) {
  console.log('ブラウザはInternet Explorerです');
}

書き方としてはこれで問題ないですが、複数のブラウザを比較する場合はIf文を記述する順序に注意が必要です。

なぜ順序に注意が必要なのか?

以下はchrome使用時のブラウザ情報ですが、後ろの方にSafariと書いてあります。
文字列「Safari」はSafariブラウザの識別子です。If文の順序を間違えるとchromeを使用しているにも関わらず、
Safariと判定される可能性があるため、If文の記述順序は注意が必要です。

Mozilla/5.0 (windows nt 6.3; wow64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

記述順序

特に「Edge」→「Chrome」→「Safari」の順番に注意して記述します。

if(userAgent.indexOf('msie') != -1 ) {
    console.log('ブラウザはInternet Explorerです');
} else if(userAgent.indexOf('edge') != -1) {
    console.log('ブラウザはEdgeです');
} else if(userAgent.indexOf('chrome') != -1) {
    console.log('ブラウザはChromeです');
} else if(userAgent.indexOf('safari') != -1) {
    console.log('ブラウザはSafariです');
} else (userAgent.indexOf('firefox') != -1) {
    console.log('ブラウザはFireFoxです');
}

参照

MDN webdocs
UserAgentからOS/ブラウザなどの調べかたのまとめ
JavaScriptでブラウザを判定して処理を条件分岐する方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AngularJSで”ng serve"立ち上げ時のエラー解決

エラー内容

Angular CLIでサーバーを立ち上げようとしたところ、
下記の様なエラーが出力され、日本語用の掲示板がなく、苦戦。

備忘録と日本語用の解決方法として記録する。

環境

  • Mac OS Catalina 10.15.7
  • zsh
  • Node.js 15.0.1
  • npm 7.0.3
  • Angular 10.0.14
  • Angular CLI 10.0.8

コード

立ち上げ

% ng serve

すると下記の様なエラーが出力された。

An unhandled exception occurred: Cannot find module '@angular-devkit/build-angular/package.json'
Require stack:
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/node_modules/@angular-devkit/architect/node/node-modules-architect-host.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/node_modules/@angular-devkit/architect/node/index.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/models/architect-command.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/commands/serve-impl.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/node_modules/@angular-devkit/schematics/tools/export-ref.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/node_modules/@angular-devkit/schematics/tools/index.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/utilities/json-schema.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/models/command-runner.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/lib/cli/index.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/lib/init.js
- /Users/username/.nodebrew/node/v15.0.1/lib/node_modules/@angular/cli/bin/ng
See "/private/var/folders/mz/z5f4vq_n6vz50y0g7vt85v4r0000gn/T/ng-7crVaj/angular-errors.log" for further details.

エラーの解決方法がググって検察してみたものの
なかなか出てこず、うまくいかず下記のサイトにてようやく答えが見つかる。
参考:Cannot find module @angular-devkit/build-angular/package.json

% npm install --save-dev @angular-devkit/build-angular

と入力したら

added 1484 packages, and audited 1484 packages in 21s

4 high severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

と出力されたので、指示通り、

% npm audit fix --force

と入力。

改めて

% ng serve

を入力したところ、

Your global Angular CLI version (10.2.0) is greater than your local
version (10.0.8). The local Angular CLI version is used.

To disable this warning use "ng config -g cli.warnings.versionMismatch false".
Compiling @angular/core : es2015 as esm2015
Compiling @angular/animations : es2015 as esm2015
Compiling @angular/cdk/keycodes : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/cdk/observers : es2015 as esm2015
Compiling @angular/animations/browser : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/cdk/platform : es2015 as esm2015
Compiling @angular/cdk/bidi : es2015 as esm2015
Compiling @angular/forms : es2015 as esm2015
Compiling @angular/router : es2015 as esm2015
Compiling @angular/platform-browser-dynamic : es2015 as esm2015
Compiling @angular/platform-browser/animations : es2015 as esm2015
Compiling @angular/cdk/a11y : es2015 as esm2015
Compiling @angular/material/core : es2015 as esm2015
Compiling @angular/material/button : es2015 as esm2015
Compiling @angular/material/toolbar : es2015 as esm2015

chunk {main} main.js, main.js.map (main) 19.7 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 141 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 168 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.2 MB [initial] [rendered]
Date: 2020-10-25T02:26:54.690Z - Hash: 3772bde64fed4e4284bc - Time: 5678ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully.

無事立ち上がった!

ps エラー解決には英語も欠かせないな

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

小学生時代の僕へ、「OCRで【いつもの作業】やっておいたよ!」

はじめに

皆さん苦手な食べ物はありますでしょうか?

私にはいくつか苦手な食べ物がありますが、その中でも「豆全般」苦手です。

え?豆?なんで?ってよく言われますが、正直なんでかはよくわかりません。
細胞が拒否している(アレルギーではありません)のかなと思います。
(こしあん、味噌、醤油OK/つぶあん、豆腐NG)

前置きはさておき、私は小学生の頃毎月配布される給食の献立表をもらったらすぐに、豆が書かれている箇所すべてにマーカーを引いていました(笑)

献立表にマーカーを引くメリットは以下の通りです。

  • 前もって豆が出てくるとわかるため心の準備ができる
  • その日の給食までに、友人に食べてほしいとお願いできる

とはいえ、毎月献立表のすべての豆にマーカーを引くのは非常に大変です。
そこで、今回はどうにか「小学生の頃の僕」を楽にしてあげたいと思います。

調べてみた

じゃあ実際にどうやるのか?と考え調べてみたところなにやら「OCR」という技術がよさげな感じがした。

OCRとは?

OCRは、Optical Character Reader(またはRecognition)の略で、画像データのテキスト部分を認識し、文字データに変換する光学文字認識機能のことを言います。具体的にいうと、紙文書をスキャナーで読み込み、書かれている文字を認識してデジタル化する技術です。
参考:業務効率ツールとして注目!「OCR」とは

OCRを使って、献立表を認識し、文字データに変換しNGワード(まめ等)をハイライトすればよさそう。
もっとスマートなやり方はあるとは思いますが、思い浮かばなかったOCRを使ってみたいのでこの方法でやっていきます。

「OCR プログラミング」と調べてみると「Tesseract」というワードがちらほら確認できました。Tesseractは、オープンソースのOCRエンジンである。
追加で調べてみると、TesseractをJavaScriptに移植したTesseract.jsがあるらしいので今回はこれを使ってみることにします。

参考:テキスト認識エンジン「Tesseract」をJavaScriptに移植した「Tesseract.js」

開発

今回は、しっかりと完成させるというよりはお試しで作ってみるだけなのでTesseract.jsはCDNで使用します。

<body>
    <div>
        <input type="file" id="uploader">
    </div>
    <div>
        進捗: <span id="progress">0</span>%
    </div>
    <div>
        <div id="ocrResult"></div>
    </div>
</body>
const files = evt.target.files;
    if (files.length == 0) {
        return;
    }

    Tesseract
        .recognize(files[0], { lang: 'jpn', tessedit_pageseg_mode: "RAW_LINE" })
        .progress(function (p) {
            // 進歩状況の表示
            let progressArea = document.getElementById("progress");
            progressArea.innerText =  p.status + " " + Math.round(p.progress * 100);
        })
        .then(function (result) {
            // 結果の表示
            $replaceResult = highlight(result)
            let ocrResult = document.getElementById("ocrResult");
            ocrResult.innerHTML = $replaceResult;
        });
}

const elm = document.getElementById('uploader');
elm.addEventListener('change', recognize);

function highlight(result) {
    const ngWords = ['そらまめ', 'ひよこまめ', 'えだまめ', 'なっとう'];
    let resultHtml = result.text.replace('', ''); //「金」を「会」と誤認してしまうため置き換える

    //NGワードにハイライト用のクラスをつける
    for (let i = 0; i < ngWords.length; i++) {
        resultHtml = resultHtml.replace(new RegExp(ngWords[i], "g"), '<span class="highlight">' + ngWords[i] + '</span>');
    }

    return resultHtml;
}

CSSは省略

今回のコードの大半はこちらのサイトのコードを使用しました。
参考:Tesseract.js を使った最小 OCR サンプル

解説

recognize

recognize(files[0], { lang: 'jpn', tessedit_pageseg_mode: "RAW_LINE" })

recognize関数は、OCR解析を実行する関数。
第一引数には対象の画像を、第二引数にはオプションを指定する。
tessedit_pageseg_modeは、ページ区切りモードを指定するオプション。RAW_LINEでページ固定している。

progress

progress(function (p) {
    // 進歩状況の表示
    let progressArea = document.getElementById("progress");
    progressArea.innerText =  p.status + " " + Math.round(p.progress * 100);
})

progress関数は、ジョブ(解析)が進行したときに呼ばれるcallback関数をセットすることが出来る。
getElementByIdでprogressの要素を取得し、progressArea変数にいれる。
innerTextでprogressAreaに解析の進行状況を反映させている。
Math.round関数で、四捨五入している。

参考
Tesseract.jsを使ってブラウザだけでOCRする方法
JS向けOCR Tesseract.jsのドキュメント和訳

ハイライト用の処理

function highlight(result) {
    const ngWords = ['そらまめ', 'ひよこまめ', 'えだまめ', 'なっとう'];
    let resultHtml = result.text.replace(new RegExp('', 'g'), ''); //「金」を「会」と誤認してしまうため置き換える

    //NGワードにハイライト用のクラスをつける
    for (let i = 0; i < ngWords.length; i++) {
        resultHtml = resultHtml.replace(new RegExp(ngWords[i], 'g'), '<span class="highlight">' + ngWords[i] + '</span>');
    }

    return resultHtml;
}

こちらは、指定の文字(NGワード)にハイライトをいれるための処理です。
今回は、豆の中でも特に苦戦を強いられる四天王をチョイスしました。

あとで説明しますが、Tesseract.jsの精度がよいとは言い切れない(やり方次第で向上すると思います)です。とりあえず今回は金曜日が会曜日になってしまうためその箇所だけ修正。

ループ処理で、NGワードをハイライト用のクラスを付けた状態に置き換えます。
この時、少し躓いたのがreplace()を使う際、以下のようにPHPと同じように記載すると

[]
const colors = ['red','blue','green'];
const replaced = colors.replace(',', ' '); 
console.log(replaced); //red blue,green 最初にマッチしたものしか置換できていない

マッチしたもの一つしかハイライトされませんでした。
正規表現を使うことで全置換を行うことが出来るんだとか。

RegExpの第二引数で指定している"g"は、グローバルマッチングのフラグでこちらを指定することでNGワードがでてくるたびにハイライトするようにできます。

参考
replaceに変数を使ってグローバルマッチさせる2つの方法
RegExp
String.prototype.replace()

then

then(function (result) {
    // 結果の表示
    $replaceResult = highlight(result)
    let ocrResult = document.getElementById("ocrResult");
    ocrResult.innerHTML = $replaceResult;
});

thenでは、ジョブ(解析)が完了したときに呼ばれるcallback関数をセットします。
結果を、先ほど説明したhighlight関数の引数に渡す。

結果

先に言いますが、うまくいきませんでした!!!温かい目で見てください。

一般的な給食の献立表

使用した画像はこちら(そもそも今回指定したNGワードはない(笑))
給食献立.PNG

懐かしいですね。※私は、あげパンが好きでした。

結果がこちら
給食献立 OCR結果.PNG

複雑すぎたか、全然読み取れていませんでした。

かなーり簡略化した特製の献立表

あの頃の自分を思うと、どうしても献立表にマーカーを引いてあげたい…。
そのため、今回は妥協して特別製の献立表を作りました。
特製献立表.PNG

9日の献立は絶望的です。

結果がこちら
献立 OCR成功.PNG

なっとうだけうまく認識できませんでしたが、その他3つのまめは無事認識できすべてハイライトできています。(所々ミスはありますが)

何度かやってみて感じたこととしては、

  • カタカナの認識は苦手そう
  • 周りの文字しだいで認識できていたものが出来なくなったりする(なつとうと認識したこともあった)
  • 細かい箇所で間違えている部分はあるがやり方次第で精度はあがりそう
  • 英語の認識精度はよさそう

おまけ(特製の献立表 改)

上記の献立表の文字を太字にしてみてやってみたら結果が変わるのか試しにやってみました。文字はくっきりしているので精度があがると予想。
特製献立表 改.PNG

結果がこちら
献立改 OCR成功?.PNG

結果は、予想に反して悪化しました。
確認してみるとえだまめの「え」、ひよこまめの「よ」、さといもの「い」など以前より大文字と小文字の区別の精度が下がっていました。

どうすれば精度はあがるのか?

◯適切な文字サイズで読み取ること
文字が小さいと認識率は下がります。画像を拡大してから読み取ることが重要。

◯高解像度の画像を使用する
人の目で確認できるレベルでも画像が粗いと認識率は下がる。できるだけ高解像度の画像を使用すること。

◯ブラックリスト、ホワイトリストを活用する
読み取る際に、絶対に使用しない文字なのにご認識されてしまうという状況があったとき、オプションでブラックリストの設定をすることで、ご認識を防ぐことが出来ます。
(今回でいうと、「金」が「会」になってしまうケース)

特定の文字しか使用しない場合は、ホワイトリストを使って指定文字のみに絞ることが出来ます。

参考:OCRの日本語読み取り精度を上げる3つの方法

まとめ

今回は、Tesseract.jsを用いて給食の献立表の指定ワード箇所をハイライトするものを作りました。
今後本格的に取り組んで、完璧なものを作り上げることがあったらまたQiitaでお伝えします。
やっぱり自分で「これ作りたい!」ってものがあると勉強も楽しくできますね。

よくよく思い出してみると、豆は本当に苦手ですがなんだかんだ給食の献立表にマーカーを引く作業は楽しんでいたような気もします(笑)。
たまには非効率な作業もありなんじゃないでしょうか!

p.s.あの頃、まめを食べてくれた関係者の皆様(友達)ありがとう!本当に助かりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3Dオブジェクトに行列を適用してX,Y,Z軸で回転させる - Three.js

rotationプロパティでの回転は、X,Y,Z軸について各1回しか回転させることができないので、複数回回転させる場合は、Matrixを使って回転させるのが楽。

See the Pen RotateByUsingApplyMatrix_ThreeJs by kob58im (@kob58im) on CodePen.

【追加】箱を転がしてみる

R,G,B(赤,緑,青)がそれぞれX,Y,Z軸

See the Pen RollingBox-ThreeJs by kob58im (@kob58im) on CodePen.

回転軸をずらしてみた。

解説

前提:
geometry(≒3Dオブジェクトの頂点の集合)に対する回転処理に関しては、positionは影響しないようである。(console.log(box.position.x)で出力した値が変化しなかった。)
なので、positionは無視して考えてよい。

X軸方向を回転軸として回転させているところの処理を図解してみる。

function rotX(degX) {
  let signDegX = (degX<0)?1:0;
  let mat;
  mat = new THREE.Matrix4().makeTranslation( 0, boxSize/2, -boxSize*(1/2 + boardZpos - signDegX) );
  box.geometry.applyMatrix(mat);
  mat.makeRotationFromEuler(new THREE.Euler( Math.PI*degX/180, 0, 0, 'XYZ' ));
  box.geometry.applyMatrix(mat);
  mat = new THREE.Matrix4().makeTranslation( 0, -boxSize/2,  boxSize*(1/2 + boardZpos - signDegX) );
  box.geometry.applyMatrix(mat);
  redraw();
}

簡単のため、例として、boxSize=1,degX>0, boardZpos=2とした場合で考える。(下記)

function rotX(degX) {
  let mat;

  mat = new THREE.Matrix4().makeTranslation( 0, 1/2, -(1/2 + 2) );
  box.geometry.applyMatrix(mat); // 回転軸にしたい頂点が原点にくるように平行移動

  mat.makeRotationFromEuler(new THREE.Euler( Math.PI*degX/180, 0, 0, 'XYZ' ));
  box.geometry.applyMatrix(mat); // X軸を回転軸としてで回転

  mat = new THREE.Matrix4().makeTranslation( 0, -1/2, (1/2 + 2) );
  box.geometry.applyMatrix(mat); // 元の位置に平行移動

  redraw();
}

image.png

図の点線部分は地面。今回のプログラムでは、yの負方向に埋もれている分をbox.position.y = boxSize/2でオフセットさせてy=0に来るようにしている。

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む