Goalist Developers Blog

Selenium WebDriverでスクリーンショットを撮る

こんにちは ゴーリスト開発のモリツグです

Selenium WebDriverで画像キャプチャが必要になった時、Firefoxだとスクロールで見えない部分まで撮影できるのに、Chromeだと見えている部分しか撮影してくれません。
この問題を解決してくれる便利なライブラリaShotのご紹介です。
言語はJavaです。

aShotの使い方

まずは以下のReadMeを参考にpom.xmlをかいたり、直接jarを落としてきたり好きな方法でaShotを使えるようにします。

github.com

Mavenリポジトリのリンクはこちらです

以下のような感じで撮影してファイルに書き出せばよいと思います。

private void captureImage(WebDriver driver, String filePath, float quality) throws IOException {
    JPEGImageWriteParam param = new JPEGImageWriteParam(Locale.getDefault());
    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    param.setCompressionQuality(quality);
        
    // 150はミリ秒でスクロールを待つ時間、短すぎるとダメな模様
    ShootingStrategy ss = ShootingStrategies.viewportPasting(150); 
    // スクロールせずにそのまま撮影したい場合は以下
    // ShootingStrategy ss = ShootingStrategies.simple();

    Screenshot screenshot = new AShot().shootingStrategy(ss).takeScreenshot(driver);

    ImageWriter writer = null;
    try {
        writer = ImageIO.getImageWritersByFormatName("jpg").next();
        writer.setOutput(ImageIO.createImageOutputStream(new File(filePath)));
        writer.write(null, new IIOImage(screenshot.getImage(), null, null), param);
    } finally {
        if (writer != null) {
            writer.dispose();
        }
    }
}

複数の画像を連結したい!

aShotとは直接関係はありませんが、複数の画像を1枚につなげて保存したいという要望があると思います。
画像サイズは同じであるとして、以下のような感じでできます。(厳密には横のサイズが1枚目と同じ前提)

private void createAllImage(WebDriver driver, String filePath) throws IOException {
    ShootingStrategy ss = ShootingStrategies.viewportPasting(150); 
    
    driver.navigate().to("https://www.google.co.jp/");
    BufferedImage image1 = new AShot().shootingStrategy(ss).takeScreenshot(driver).getImage();
    
    driver.navigate().to("https://goalist.co.jp/");
    BufferedImage image2 = new AShot().shootingStrategy(ss).takeScreenshot(driver).getImage();
    
    JPEGImageWriteParam param = new JPEGImageWriteParam(Locale.getDefault());
    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    param.setCompressionQuality(1.0f);
    
    List<BufferedImage> captureList = new ArrayList<BufferedImage>();
    captureList.add(image1);
    captureList.add(image2);

    int entireHeight = 0;
    for (BufferedImage imageBuf : captureList) {
        entireHeight += imageBuf.getHeight();
    }
    BufferedImage entireImage
        = new BufferedImage(captureList.get(0).getWidth(), entireHeight, captureList.get(0).getType());

    ImageWriter writer = null;
    Graphics entireGraphic = null;
    try {
        entireGraphic = entireImage.getGraphics();
        int tempHeight = 0;
        for (BufferedImage imageBuf : captureList) {
            entireGraphic.drawImage(imageBuf, 0, tempHeight, null);
            tempHeight += imageBuf.getHeight();
        }
        writer = ImageIO.getImageWritersByFormatName("jpg").next();
        writer.setOutput(ImageIO.createImageOutputStream(new File(filePath)));
        writer.write(null, new IIOImage(entireImage, null, null), param);
    } finally {
        if (writer != null) {
            writer.dispose();
        }
        if (entireGraphic != null) {
            entireGraphic.dispose();
        }
    }
}

まとめ

ブラウザに関係なくお手軽にスクリーンショットが取れるaShotが本当に便利でした。
スクロールバーは出ているけれど、このパターンの時はスクロールしなくていいなぁみたいな時にShootingStrategyでスクロールさせない方法が指定できるのも使いやすかったです。

【知ってるようで知らない】AWS EC2インスタンスのしくみ

こんにちは、ゴーリスト開発の飯尾です

最近AWSからこんなお知らせが来てなんじゃいと思いましたが
どうやらとあるインスタンスがもうすぐ死んでしまうらしい

f:id:y-iio:20180213162724p:plain

このあたり見るにつけ

docs.aws.amazon.com

qiita.com

ルートデバイスタイプがEBSなので、いったんインスタンス停止して開始したらOK〜

はい

よく考えたらルートデバイスタイプって何?なんで再起動したらOKなの?

EC2インスタンス日々使っているくせに
どういう仕組みで動いてるのかはぜんぜん知らなかったので調べてみました

そもそもEC2ってなんだ

EC2ってなんだ
仮想のサーバーをゾンアマさんから借りれるしくみだよ

インスタンスってなんだ
借りた仮想のマシンだよ
ホストコンピュータの一部分を一つのマシンとして扱うよ
ここではホストコンピュータ君の一部に肉体の型と魂を与えて一つのマシンとみなすよ

f:id:y-iio:20180213163049j:plain:w300

リージョンってなんだ・アベイラビリティゾーンってなんだ
ホストコンピュータが物理的に置いてある国・地域の分別のことだよ

f:id:y-iio:20180213163109j:plain

AMIってなんだ
インスタンスを作るためのテンプレートだよ
アマゾンマシンイメージの略だよ
マシンの肉体の型だよ

f:id:y-iio:20180213163135j:plain

EBSってなんだ
マシンのデータ置き場のひとつと思っておこう
ここではデータ=魂ということにするよ

f:id:y-iio:20180213163202j:plain:w300

肉体の型と魂さえあればどのホストコンピュータ上にも同じマシンを作れるよ

ルートデバイスタイプってなんだ

インスタンスはテンプレートを使って起動されるよ

昔はテンプレート置き場がS3にあって、インスタンス起動のたびに
それぞれのホストコンピュータのインスタンスストアにそれをコピってきて使っていたよ
データボリュームはその都度ホストコンピュータ上に作られているから
インスタンスを停止したらデータは失われるよ
「ルートデバイスタイプがインスタンスストア」っていうのはこういうことを言うよ

f:id:y-iio:20180213163241j:plain

今ではEBSというしくみがあって、
EBSベースに作られたテンプレートを使って、EBSに置いてある魂を紐づけて使ったりするよ
インスタンスを停止してもデータはEBS上に残るよ
「ルートデバイスタイプがEBS」っていうのはこういうことを言うよ

f:id:y-iio:20180213163305j:plain

docs.aws.amazon.com

なんで再起動したらOKだったんだ

ゾンアマさんからきた死亡宣告は、
「今動いてるインスタンスのホストコンピュータが調子悪いぜ!」
と言う意味だったよ

f:id:y-iio:20180213163422j:plain:w300

なのでいったん停止してまた起動し直すことで
別のホストコンピュータ上でインスタンスが起動したからOKになったよ

f:id:y-iio:20180213163357j:plain:w300

はい

「正解率」でモデルを評価することは危険(かもしれません)!

こんにちは、ゴーリストのチナパです! 機会学習の中によくあるクラシフィケーション問題を作りながら必ず「正解率」と出会うでしょう。しかし、アルゴリズムを評価する方法は他でもあります。ここにはいくつかを調べて見ます。

まず、問題を定義しましょう。簡単な可塑的な問題です。10人の中、誰が赤が好き、そして誰が青いが好きを何かの情報を見て予想したい設定を使います。 つまり、一番の人から10番の人まで、「赤」か「青」というラベルを付けたいということになります。

f:id:c-pattamada:20180208192435p:plain

では、こんな結果がでたとしましょう。 一番自然な評価は正解率だと思いますので、まずはそこを見ていきましょう ここは7回●、3回✖️なので、正解率は70%ですね。

ただし、よく見るとこのアルゴリズムが青を全然よく判断してありません。それは、もしかして青が少数だからかもしれませんが、この問題をもっとひどくした場合に こういうことも見れます。

f:id:c-pattamada:20180208192502p:plain

ここも正解率70%です 単なる毎回「赤」と言っているじゃないですか、こんなことをするたみにアルゴリズム作る必要もないです。 70%正しいでも、本当の状況について何も行ってくれません。

これは正解率の弱点です、クラス(この場合、赤と青)の割合がほぼ同じだった場合なら問題ないかもしれませんが現実の世界では 数が多いクラスと数が少ないクラスが大抵のものです。

では正解率より良い方法があるのでしょうか?

一つの方法は正解と間違った率に加えて偽陽性と偽陰性の割合も見ることです。 自分は辞書から探し出した言葉なので説明します(難しい言葉がどうかわかりません) 「赤」の偽陽性はアルゴリズムが「赤」だと思ってて間違ってた時 「赤」の偽陰性はアルゴリズムが「赤」ではないと思ったが実は赤でした時

2番目の例には、青の偽陰性が100%なので一目で問題があるとわかります。 1番目の例の場合の詳しい計算をします、都合よくするためにもう一回貼ります

f:id:c-pattamada:20180208192435p:plain

赤: 答えた数:8 実は赤だった数:7 正解数:6 偽陽性数:2 偽陰性数:1 こんな感じでも表せられます。

f:id:c-pattamada:20180208192539p:plain

こういう情報も分類するモデルの作成の時にとても便利です。(緑は正しかった時、オレンジは間違ってた時.....ここも赤と青使ってたら混乱しますねw)

では、比べられるために一つの数値で表せられませんか? 様々な方法があると思いますが、私は便利だ思うのはf-value (か、f1-value)と言われます。

(precision・精度) = 偽陽性数 / 答えた数 = 2 / 8 => 25%
(recall・再現率)= 正解数 / 実は赤だった数 = 6 / 7 => 86%

赤の再現率はアルゴリズムが「赤だ」ということをどれだけ上手く予想できるかを測る数字 赤の精度はアルゴリズムが「赤ではない」ということをどれだけ上手くわかるかを測る数字

この両方は高ければ、アルゴリズムが「赤」をよく予想できると言えるでしょう。 では、どう組み合わせますか?

f-valueは精度と再現率の調和平均です。

つまり

f-value = 1 / ((1 / 精度) + (1 / 再現率))

この場合の赤のf-valueは39%です。 ちなみに、青のf-valueは40%です。それぞれのクラスのf-valueの平均をとればアルゴリズムのf-value を表せます。この場合、はもちろん40%以下です、70%の正解率をみるより焦りますね。

f:id:c-pattamada:20180208193402j:plain

(写真はbimbimkhaより)

Angularでenvironmentsの中身をいじって環境毎に変数の中身を設定したい

おはようございます。バンナイです。 Angularでenvironmentsの中身をいじって環境毎に変数の中身を設定したいです。

動機

今度新しく作るステージング環境で呼び出すAPIを既にある本番環境で呼び出すAPIとは別にしたい。

デプロイしたい環境に応じてAPIのURLを手作業で書き換えることもできる。

しかし、environmentsの中身をいじればビルドをする際のコマンドを変えるだけでそれが実現できると先輩から教えてもらったのでそれを試してみます。

作業

environments配下にファイルを作成、編集

src/environments 配下に environment.staging.tsを作成。中身は

export const environment = {
  production: false,
  apiUrl:"staging環境でのurl"
};

environment.dev.tsの中身は

export const environment = {
  production: false,
  apiUrl:"dev環境でのurl"
};

environment.prod.tsの中身は

export const environment = {
  production: false,
  apiUrl:"prod環境でのurl"
};

それぞれ以上のようにします。

.angular-cli.jsonの内容を編集

environmentsのところの内容を以下のようにします。

"apps": [
    {
     ・・・略
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        "staging": "environments/environment.staging.ts"
      }
    }
  ]

environment を import

変数を利用したい箇所でenvironment を importしたり等。 今回はテストのためにapp.component.tsで環境毎に中身が変わる変数を用意して、変数の中身をapp.component.htmlで画面に表示して、環境毎に変数の中身がちゃんと書き換わっていることを確認します。

app.component.ts

・・・略
import {environment} from "../environments/environment";
・・・略
export class AppComponent {
  public apiUrl = environment.apiUrl;
・・・略

app.component.html

<p>{{apiUrl}}</p>

確認

ローカル環境で確認してみます。

ng s --env=dev
ng s --env=prod
ng s --env=staging

で動かしてみて、それぞれ画面に

dev環境でのurl

prod環境でのurl

staging環境でのurl

と表示されたらオッケーです。

f:id:bbbbbbbbb9:20180208134351p:plain

実際にデプロイする時は

ng build --env=staging

等とすればデプロイしたい環境に応じて、コードを書き換えることなく、変数の中身を変更できます。

統計・マーケ・機械学習 Meetup! #4 @ ゴーリスト

そういえば,数か月ぶりの執筆担当だ....

お久しぶりです.新卒エンジニアのナカノです.

今回は,ゴーリストオフィスで行われた勉強会のレポートを書こうと思います.

勉強会のテーマは「統計・機械学習」です.

目 次
  • 勉強会の概要
  • 何はともあれ、まずはカンパーイ!
  • LTの内容
    • 1. 使えるデータの作り方
    • 2. DBスペシャリストに合格して自由を手に入れた話
    • 3. AICについて
    • 4. 統計検定を受けてみた
    • 5. pythonとmatlabを使った最適化とこれから読みたい本
    • 6. GoogleのAI分析の結果を、実地で確かめてみた
    • 7. 機械学習初心者が短期間でTensorFlowで作ったMNISTモデルをAndroidアプリに組み込んだ話
    • 8. IT勉強会開催の進めと「続ける」技術
    • 9. ピクトさん量産計画
    • 10. 分析手法紹介
  • 締め括り



勉強会の概要

以下のURL先のページに,勉強会の概要が書かれております.一度ご覧頂ければと思います. data-refinement.connpass.com



何はともあれ、まずはカンパーイッ!

Meetupということで,飲み食いしながら様々なLTが堪能出来る形式となっております.

さてさて,まずは各自飲み物を持って,せーの......カンパーイッ!

f:id:r-nakano:20180125015027j:plain

思いの外参加者が多く,その分だけこれから始まるLTの発表に胸が高鳴るナカノでした.

お~,ドキがムネムネッ!(某クレヨンし○ちゃんのネタ)



LTの内容

各LTの発表のまとめを,self-containedな形で以下に与えておこうと思います.

1. 使えるデータの作り方

f:id:r-nakano:20180125155328j:plain

主に,データの必要不可欠さとデータの加工に関することを発表されておりました.

何事においてもデータは必須であり,それを有効活用するためには「必要な時に必要な形」でデータが使える様にする必要があります.

そのために,CrawlingやScrapingという技術を用いてデータの加工整形が行われます.


2. DBスペシャリストに合格して自由を手に入れた話

f:id:r-nakano:20180125022557j:plain

発表によると,即戦力になるためにはやはり少なくとも資格を取得しておいた方が良いとのことです.

ですがその一方で,mqtsuo02さん自身が様々な業務を経験してきた結果から,資格の取得だけでなく「自分次第な部分を変えるための実践」も非常に大切だと説いていらっしゃいました.


3. AICについて

f:id:r-nakano:20180125135827j:plain

相関係数の話から発表は始まり,次第にAICに関する様々なツールや概念を概説されていらっしゃいました.

その時,聴講者たちはどんな表情をしていたのでしょうか...?

f:id:r-nakano:20180125140752j:plainf:id:r-nakano:20180125140801j:plain

おっと,何とも言えない表情をされていらっしゃるゾ....


4. 統計検定を受けてみた

f:id:r-nakano:20180125135915j:plain

cougarさんの「フリーランスになった経緯」から「統計検定を受けた時の状況」などについて,発表されておりました.

統計や機械学習がビジネスで活かせそうだとcougarさんは考えており,特に「顧客サービスの休止ユーザーに関する分析」への応用に期待していらっしゃるそうです.


5. pythonとmatlabを使った最適化とこれから読みたい本

飛び込みLTの発表その1です.発表者のmoritaさんは「人工衛星により捉えた生物の住む森林のエリアの画像を分析し,そのエリアの面積の変化を調査する」ということを行ってらっしゃるそうです.

その際にpythonやmatlabを使った最適化処理を実施されているのですが,現状ではデータ分析の方法や手順や方法が確立されているが分析精度の大小などは評価しづらいとのことです.

それ故にデータを観察し想定や着想などを持つことが大事であり,それは機械学習の利用においても言えることであるそうです.

また,発表の最後にオススメの関連書籍を紹介されておりました.


6. GoogleのAI分析の結果を、実地で確かめてみた

飛び込みLTの発表その2です.発表内容は,Googleの「スマートクッキー」プロジェクトに関するものでした.AI分析の結果として出力されたレシピは非常にシンプルでした.

AIの判定を実験的に確かめるため,そのレシピをもとにいざ作ってみたそうです.結果ですが,レシピに改善すべき点が幾つかみられました.

判明したことですが,AIのアウトプットは業務に落とし込む部分の手前のものなので,やはりその様な部分で人の役割は必要不可欠であるということです.


7. 機械学習初心者が短期間でTensorFlowで作ったMNISTモデルをAndroidアプリに組み込んだ話

発表内容は「数字が写っている画像を認識して正解の数字を特定するアプリの開発」に関してです.

開発の際に,Kerasという「Tensor Flowなどをバックエンドとしたニューラルネットワーク用ラッパーライブラリ」を使用しているとのことです.

まえすとろさんによれば,Kerasは初学者でもアプリ開発で何とかなる有能なツールだそうです.


8. IT勉強会開催の進めと「続ける」技術

発表によると,IT勉強会の開催のメリットとしては「勉強会はやりとりや知識・認識の共有がしやすい環境であるため,参加する意義がある」とのことです.

また,継続は力なりということも説いていらっしゃいました.

例えば「30日間をどのように続けるか?」とか「反抗期、不安定機、倦怠期などの作業継続が困難になりがちなフェーズに陥った場合,如何にして乗り越えるか?」などの話がありました.

聞いているうちに,継続は力なりという言葉は非常にシンプルだが大変重要だなと再認識しました.


9. ピクトさん量産計画

写真からのピクトの生成に関して発表されておりました.

ピクトの生成の際にDeep Learningによるポーズ推定が行われており,その結果として「画像にある人のポーズから様々なピクトの生成が可能」となっております.

ポーズ推定ですが,対象が一人或いは複数人の場合のそれぞれで推定を行います.

複数人の場合は,まずはそれぞれの部分を切り取り,そこから先は「一人の場合の推定方法」を使って推定が行われます.

推定の実験の中で,何故か大阪のグリコの画像からはピクト変換出来なかったそうです.


10. 分析手法紹介

最後の発表は,様々な分析手法の紹介に関してでした.発表の中で

  • 数理最適化(近藤次郎(最適化))、探索問題(近藤次郎(最適化))
  • 応用待ち行列(本間鶴千代(待ち行列の理論))
  • 世論調査モデル(深尾毅(分散システム論))
  • 昇給報酬モデル(羽鳥裕久(有限マルコフ連鎖))

といったものが紹介されていました.



締め括り

以上が,勉強会で発表された内容のまとめでした.各々のまとめの内容に凹凸があり,非常に稚拙なまとめとなってしまいました.

ただ,勉強会の状況や様子をお伝えすることは出来たかなと思っております.

私は現在インフラ関連の業務を行なっておりますが,統計・機械学習・関数型言語の分野にも興味があるため,分野に囚われずに積極的に勉強していければと思います.

SQLAlchemyでsetattrしたのにupdateされない

こんにちは ゴーリスト開発のモリツグです

最近ちょっとずつPythonを触り始めたひよっこです。
Python3.6でSQLAlchemyを使っていて何故かupdateが行われない現象に悩まされたので、原因と対処法をメモしておこうと思います。

ぶちあたった問題

sessionの取得やcommit flush closeあたりは適切に行われているものとして省略します。 あとPythonなのにキャメル記法なのは一旦みなかったことにしてください。

・・・(sessionの取得)・・・

# 更新したいレコードを取得
dbAccount = session.query(Account) 
    .filter(Account.id == account.id) 
    .first()

# 更新したい値を持っているaccountからdbAccount へ値をコピーして反映
for key, value in account.__dict__.items():
    if callable(value) == False: # 関数は省く
        setattr(dbAccount, key, value)

・・・(commit flush closeの処理)・・・

上記のようなソースコードを書いた場合、デバッガ上ではdbAccount の値が更新されているのに、DB上の値は更新されません。
試行錯誤した結果、setattrのvalueをちゃんとキャストしてやるとDB上の値も更新されました。

# 更新したい値を持っているaccountからdbAccount へ値をコピーして反映
for key, value in account.__dict__.items():
    if callable(value) == False: # 関数は省く
        # setattr(dbAccount, key, value) # valueの型を判定してキャスト
        if isinstance(value, int):
            setattr(updateEntity, key, int(value))
        elif isinstance(value, str):
            setattr(updateEntity, key, str(value))
    ・・・(それ以外の型の処理)・・・

何故こんなことになっているのか、詳しい理由は不明ですが、きっと何か深淵なる理由があるのだと思います。

Observableでクリックイベントを制御してドロップダウンメニューを作ってみる【Angular】

こんにちは。ゴーリスト開発の飯尾です。
AngularでWebアプリを作ったりしています。
世の賢人によるイルなコンポーネントだと微妙に機能が多すぎたり足りなかったりで
結局自作のワックコンポーネント生産したりしますよね。

作りたいもの

  • Airbnb一休の検索条件指定部分みたいなドロップダウンメニュー
  • 内側クリックで閉じず、外側クリックで閉じる
  • 一画面でひとつだけ開く

イメージとしてはこんなやつです。

f:id:y-iio:20180109180913g:plain

参考にしたもの

基本的にはこれパクっ参考にしましたが
ここのところがちょっとな〜だったので書き換えました。

@HostListener('click', ['$event']) onClick(event: Event) {
    if (!this.hideOnClick) {
        event.stopPropagation();
    }
}

変えたいポイント

その1

ドロップダウンを開いている時のみクリックイベントを拾いたい(@HostListenerでイベント登録するとリムーブできない)

github.com

その2

iOSだとdocumentをルート要素としてクリックイベントを登録しても拾ってくれない

qiita.com

解決法を考える

ドロップダウンを開いている時のみクリックイベントを拾いたい 問題

Observableでクリックイベントを制御する

qiita.com

// ドロップダウン開いてる時
const stream = Observable.fromEvent(document, 'click').subsucribe(() => {
  console.log('clicked!');
});

// 閉じたら購読をやめる
stream.unsubscribe();

iOSだとdocumentをルート要素としてクリックイベントを登録しても拾ってくれない 問題

Observable.fromEvent(document, 'touchend')ならOK

そうしてできたもの

import {
  ChangeDetectorRef, Component, ElementRef, HostBinding, Injectable, OnDestroy, OnInit, ViewEncapsulation
} from '@angular/core';
import { Observable, Subscriber } from 'rxjs/Rx';

@Injectable()
export class DropdownRegistry {
  private dropdownMenuComponents: any[] = [];

  constructor() { }

  public add(dropdownMenu: DropdownMenuComponent) {
    this.dropdownMenuComponents.push(dropdownMenu);
  }

  public remove(dropdownMenu: DropdownMenuComponent) {
    this.dropdownMenuComponents.slice(this.dropdownMenuComponents.indexOf(dropdownMenu), 1);
  }

  public hideAllExcept(dropdownMenu: DropdownMenuComponent) {
    this.dropdownMenuComponents.forEach((component) => {
      if (component !== dropdownMenu) {
        component.hide();
      }
    });
  }
}

@Component({
  selector: 'app-dropdown-menu',
  template: `<ng-content></ng-content>`,
  encapsulation: ViewEncapsulation.None // ここはカプセル化しない
})
export class DropdownMenuComponent implements OnInit, OnDestroy {
  @HostBinding('class.is-visible') isVisible: boolean = false;

  private clickEventStream: any; // Observable<Event>とかObservable<any>だと警告出る
  private subscription: Subscriber<any>;

  constructor(
    private changeDetectionRef: ChangeDetectorRef,
    private dropdownRegistry: DropdownRegistry,
    private elementRef: ElementRef,
  ) {
    this.dropdownRegistry.add(this);
  }

  ngOnInit() {
    // iOSだとボタンとかリンクの要素以外はタッチしてもクリックイベントとしてみなされない
    this.clickEventStream = Observable.merge(Observable.fromEvent(document, 'click'), Observable.fromEvent(document, 'touchend'))
      .catch(e => Observable.throw(e));
  }

  ngOnDestroy() {
    this.dropdownRegistry.remove(this);
  }

  public toggle(event: Event): void {
    if (this.isVisible) {
      this.hide();
    } else {
      this.show(event);
    }
  }

  public hide(): void {
    this.isVisible = false;
    if (this.subscription && !this.subscription.closed) { // stream流れてるなら止める
      this.subscription.unsubscribe();
    }
    this.changeDetectionRef.markForCheck();
  }

  public show(event: Event): void {
    event.stopPropagation();
    this.hideAll();
    this.isVisible = true;
    this.subscription = this.clickEventStream.subscribe((clickEvent: Event) => { // 外側クリック時に閉じる
      clickEvent.stopPropagation();
      if (this.isVisible && !this.elementRef.nativeElement.contains(clickEvent.target)) {
        this.hide();
      }
    });
  }

  private hideAll(): void {
    this.dropdownRegistry.hideAllExcept(this);
  }

}

あとはドロップダウン表示用のトグルボタンとCSSをいいかんじにする
いいかんじにする

こんな感じで動いているよ

f:id:y-iio:20180109181027g:plain

おわりに

もっとドープな書き方を教えてくれるマイメンを募集しています!!