cytoscape.jsとslack APIでチーム内の関係を可視化

こんにちは増島です! 久しぶりの投稿です!

職場の近くに塚田農場があるのですが、ランチにラーメンを出していてずっと気になっていました。 いつも結構混んでるので避けていたのですがふと行ってみようと思い行ってきました。

居酒屋さんですので味はそれほど期待していなかったのですが・・・ 塚田農場は神でした。

さて、職場のコミュケーションは大事ですよね! 退職原因で一番多いのは人間関係とはよく言われますし、社内のコミュニケーション不全には早めに対処したいものです。

とはいえ人間関係の把握は難しいもので、なかなか手を打ちづらく時間がかかります。 そこで今回は普段職場で使っているチャットツールからちょろっとコミュニケーションを可視化してみようという試みです。

今回は使用するチャットツールはslackです。オシャレなUIでエンジニアやデザイナーがいる会社とかだと使われる事も多いんじゃないでしょうか。 slackはAPIを公開しているので、過去に投稿されたメッセージを取得し色々と分析するなどが可能です。

作るのもの

slackにはメンションという機能があり、オープンなチャンネル内でも「誰に対して送っているのか」を表現できます。
今回はメンションの付けられたメッセージを利用し、「誰が」「どの程度」「誰に」メッセージを送信しているのかを ソーシャルグラフで表現します。

cytoscape.jsとは

オープンソースのグラフ構造可視化ライブラリ。 これを使うとチョロっとデータを与えるだけで、非常に簡単にお洒落なソーシャルグラフを描画してくれます。 http://js.cytoscape.org/

slack APIで必要情報をとる

それでは実装していきましょう。今回はNode.jsを使用して実装していきます。 動作確認を実施した環境はv8.9.0になりますが、v8以降であれば問題ないと思います。

ソースコードはところどころかいつまんで載せますが、全部を見たい場合は下記を参照してください。 https://github.com/shushutochako/slack-social-graph

slack API Tokenの取得

slackAPIへのリクエストにはSlackから発行されたトークン必要になります。
slackにwebでログインした状態で下記のページへアクセスすると、トークンを発行できます。
https://api.slack.com/custom-integrations/legacy-tokens
※ このページで発行するトークンは開発などでテスト用にサクッと作成するもので、正式なトークンでは無いようです。

グループ内メンバーの取得

https://slack.com/api/」 にリクエストすることによって、色々な情報を取得したり更新することができます。 今回はslackAPIにリクエストする部分の処理についてはSlackClientといった名前でクラスを作成しています。

グループに所属するメンバーのプロフィールを取得するにはusers.listのapiにGETリクエストします。 パラメータにtokenを指定してリクエストを実行します。

今回表示するソーシャルグラフではチャンネル内のメンバーを取得するAPIの情報をベースにしますが、そのAPIから取得する情報にはメンバーの名前が含まれないので、こちらで別途取得しています。

'use strict'

const util = require('util');  
const request = util.promisify(require('request'));

const baseURL = 'https://slack.com/api';

class SlackClient {  
  constructor(config) {
    this._channelId = config.channelId;
    this._token = config.token;
  }

  async getUsers() {
    const options = {
      uri: `${baseURL}/users.list`,
      qs: {
        "token": this._token
      }
    }
    const response = await request(options);
    const body = JSON.parse(response.body)
    if (!body.ok) {
      throw new Error('users.list request NG');
    } else {
      return body.members;
    }
  }
}

module.exports = SlackClient;  

リクエストの結果はbodyのokパラメータで判定することができます。 okがfalseの場合はリクエスト失敗になるのでエラーにしています。

if (!body.ok) {  
  throw new Error('users.list request NG');
}

チャンネル参加メンバーの取得

チャンネル内にいるメンバーを取得するには「conversations.members」apiを使用します。 パラメータにtoken、チャンネルIDを指定してリクエストを実行します。

グループ内メンバーの取得とほとんど同じなので、簡単ですね!

async getChannelMembers() {  
  const options = {
    uri: `${baseURL}/conversations.members`,
    qs: {
      "token": this._token,
      "channel": this._channelId
    }
  }
  const response = await request(options);
  const body = JSON.parse(response.body)
  if (!body.ok) {
    throw new Error('conversations.members request NG');
  } else {
    return body.members;
  }
}

チャンネル内メッセージの取得

チャンネルのメッセージを取得するには「channels.history」apiを使用します。

async getChannelHistory() {  
  const options = {
    uri: `${baseURL}/channels.history`,
    qs: {
      "token": this._token,
      "channel": this._channelId
    }
  }
  const response = await request(options);
  const body = JSON.parse(response.body)
  if (!body.ok) {
    throw new Error('channels.history request NG');
  } else {
    return body.messages;
  }
}

取得処理の呼び出し

上で実装したapiからの取得モジュールを使用して情報を取得します。

const SlackClient = require('../logic/slack-client');

・
・

(async function () {
  // user情報の取得
  const users = await client.getUsers().catch((err) => { res.send('err'); });

  // チャンネル参加メンバーの取得
  const channelMembers = await client.getChannelMembers().catch((err) => { res.send('err'); });

  // チャンネル内メッセージの取得
  const channelHistory = await client.getChannelHistory().catch((err) => { res.send('err'); });
  const messages = channelHistory.filter((elm) => {
    return elm.type === 'message' && elm.subtype !== 'file_share'
  });
})();

ファイルシェアなどの履歴は対象外とし、純粋なメッセージのみとしています。

const messages = channelHistory.filter((elm) => {  
  return elm.type === 'message' && elm.subtype !== 'file_share'
});

cytoscape.jsのソーシャルグラフを描画

cytoscape.jsを使用して描画を使用する際はcytoscapeのインスタンスを生成してdivタグに埋め込むだけで可能です。
syleを設定することによって線の色や形などを柔軟にカスタマイズする事が可能です。

<body>  
  <div id="cy"></div>
  <script>
    var cy = cytoscape({
      container: document.getElementById('cy'),

      layout: {
        name: 'cose',
        padding: 50
      },

      style: cytoscape.stylesheet()
        .selector('node')
        .css({
          'shape': 'data(shape)',
          'width': 'mapData(weight, 0, 100, 0, 100)',
          'content': 'data(name)',
          'text-valign': 'center',
          'background-color': 'data(backGroundColor)',
          'color': '#fff'
        })

       ・
       ・
       ・

      elements: <%-JSON.stringify(elements) %>,

      ready: function () {
        window.cy = this;
      }
    });
  </script>
</body>  

描画に使用する情報はelementsプロパティに渡します。 elementsの形式は下の通りとなります。

{
  elements: {
      nodes:${ノード情報の配列},
      edges:${エッジ情報の配列}
}}

大まかにノード情報(登場人物の情報)とエッジ(線の情報)が必要で今回は内容をサーバサイド側で生成していますので、 そちらを一つづついきましょう。

ノード情報の作成

ノード情報の形式は下記の通りとなります。

[ { data:
     { id: 'XXXXXX1',
       name: '太郎' } },
  { data:
     { id: 'XXXXXX2',
       name: '二郎' } },
  { data:
     { id: 'XXXXXX3',
       name: '三郎' } }
]

まずは点の情報に関してユニークなIDをidに設定する必要があります。

その他描画に使用する情報をカスタムプロパティで設定しておくことにより、 描画時にcytospace.js側のプロパティとマッピングする事が可能です。

今回だと描画に使用される名前をnameに設定しておき、 html側でcontent要素にマッピングしています。

このフォーマットに沿って今回描画に使用する情報を生成していきます。

// ノード情報の作成
const nodes =  channelMembers.map((elm) => {  
  const node = new Object();
  node.id = elm;
  users.map((user) => {
    if (elm === user.id) {
      node.name = user.name;
    }
  });
  node.weight = 100;
  const colorIndex  = Math.floor(Math.random() * (colors.length - 1));
  node.backGroundColor = colors[colorIndex];
  node.shape = 'ellipse';
  return {
    data: node
  };
});

ノードのベースとなる情報としてconversations.membersから取得した情報を使用しています。 conversations.membersのレスポンスにはメンバーの名前が含まれていないので、
users.listから取得した情報を使用してnameを付加しています。

エッジ情報

エッジ情報は下記の形式になります。

[ { data:
     { source: 'XXXXXX1',
       target: 'XXXXXX2',
       width: 7 } },
  { data:
     { source: 'XXXXXX2',
       target: 'XXXXXX1',
       width: 1 } },
  { data:
     { source: 'XXXXXX2',
       target: 'XXXXXX1',
       width: 4 } }
]

ノードと大まかには似たような構成になります。

sourceプロパティには矢印の原点にあたるノードのIDを指定します。
そしてtargetプロパティには矢印の宛先にあたるノードのIDを指定します。 これだけでノードからノードへの矢印をcytoscape.jsが描画してくれます。

エッジ情報もカスタムプロパティを設定する事ができ、今回だとwidthに値を設定して cytoscape側のwidthとマッピングしています。エッジのwidth情報は矢印の太さを表現します。

このフォーマットに沿って今回描画に使用する情報を生成していきます。

// エッジ情報(矢印)の情報の作成
const messageObject = new Object();  
const mensionMessages = messages.map((elm) => {  
  const text = elm.text;
  const mensions = text.match(/<@(.+?)>/g);
  if (mensions) {
    if (!messageObject[elm.user]) {
      messageObject[elm.user] = new Object();
    }
    mensions.map((_elm) => {
      const replaced = _elm.replace('<@', '').replace('>','');
      if (!messageObject[elm.user][replaced]) {
        messageObject[elm.user][replaced] = 1
      } else {
        messageObject[elm.user][replaced] += 1;
      }
    });
  }
});

const edges = new Array();  
Object.keys(messageObject).forEach((source) => {  
  Object.keys(messageObject[source]).forEach((target) => {
    edges.push({
      data: {
        source: source,
        target: target,
        color: '#494747',
        width: messageObject[source][target]
      }
    })
  });
});

ベースとなる情報はchannels.historyです。

text項目がメッセージ情報になり、ンションの情報は<@ >で括られている部分になり、中の文字列が宛先のユーザIDになります。

メッセージの履歴からメンションの付いたメッセージ情報を正規表現で抽出した上で、 「宛先」「メンション相手」の組み合わせ毎にカウントします。 カウントした値を最終的にwidthとして設定し線の太さで表現しています。

表示する

さて実装ができたところで表示してみましょう。 こちらは僕が普段使っているスラックグループの特定チャンネルにおけるソーシャルグラフです。

EさんとFさんはお互いに矢印が太くなっており、
コミュケーションの頻度が高い事がわかります。

また、BさんとCさんは誰ともコミュニーションを取っていませんね。 社内チャットの出現頻度が低い人は退職率が高いというデータもあるようで、 BさんとCさんについてはヒアリングを実施するなど、何か対策を練る必要があるかもしれません。

普段のコミュニーションツールなどを利用し、簡単にチーム内の状況を可視化してみることによってチーム内の関係を把握するきっかけになるかもしれませんね!

以上、cytoscape.jsとslack APIでチーム内の関係を可視化でした!