Tableau拡張機能の作り方

Tableau拡張機能の作り方 | Tableau-id Press -タブロイド-
tableau-logo-1200x630-960x504-1-e1628055423558

前回せっかくTableau拡張機能を作って遊んだので、Tableau拡張機能の作り方を解説しようと思います。
今回は、Tableau公式リポジトリのサンプルコードから必要最低限のコードを抜き出して拡張機能を作っていきます。
この記事で作成する拡張機能はこちらのGithubに公開しています。

意外と中身はシンプルで、APIも触りやすいってことを共有できればと思います!
※JavaScriptやHTML/CSSに関する知識が必要になります。

作るもの

ダッシュボードに含まれるデータソースを一覧表示し、それぞれを更新するボタンを描画します。

必要なもの

  1. HTMLファイル
  2. JavaScriptファイル (HTMLファイルに直で書くなら不要)
  3. TREXファイル(Tableau拡張機能のマニフェスト)
  4. Webサーバ(つながるならどこでもOK。今回はGithub Pagesを使ってます)

HTMLファイル

ヘッダで必要なCSSやJSのライブラリを読み込みます。

  1. jQuery
    ・自分の拡張機能コードで使います。
  2. Bootstrap
    ・見た目の調整
  3. Tableau Extension API
    ・これを先に読み込んで、Tableau Extension APIを使えるようにします。
    ・CDNのホスト情報は公式リファレンスでは見つけることができませんでした。フォーラムでTableau内部の方がCDNについて答えてくれているので、そのURLを使用します。
    ・ちなみに、公式のGithubリポジトリにはlibディレクトリにAPIのjsファイルがあります。
  4.  main.js
    ・後で作成する自分のjsコード

    JSファイルから操作するテーブル要素についても先に書いておきます。

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="UTF-8">
        <title>シンプルな拡張機能</title>
    
        <!-- jQuery -->
        <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    
        <!-- Bootstrap -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    
        <!-- Tableau Extention API -->
        <!-- CDNの情報はhttps://community.tableau.com/s/question/0D54T000006B4NASA0/tableau-extension-cdn-->
        <script src="https://extensions.tableauusercontent.com/resources/tableau.extensions.1.latest.min.js"></script>
    
        <!-- 自分のスクリプト -->
        <script src="main.js"></script>
    </head>
    
    <body>
        <div class="container">
            <h4>データソース一覧</h4>
            <table id="dataSourcesTable" class="table table-striped">
                <thead>
                    <tr>
                        <th>ソース名</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
    </body>
    
    </html>
    

    JavaScriptファイル

    tableau.」で始まる呼び出しが全てTableau Extension APIの操作になります。
    (当然ですが、そこから得たオブジェクト操作についても)

    ダッシュボード内のデータソースの全取得は最初の10行ほどで行っています。
    更新ボタンについては後半の「refreshButton.addEventListener」で、クリック時にTableau APIのデータソース更新「refreshAsync」を呼びだすようにしています。

    // DOMの準備ができたら実行。$(document).readyと同義
    $(async function(){
        // Tableau拡張の初期化処理
        await tableau.extensions.initializeAsync();
    
        // ダッシュボード情報の取得
        const dashboard = tableau.extensions.dashboardContent.dashboard;
    
        // ダッシュボードに含まれる全てのワークシートから、データソースの情報を取得
        const dataSourceFetchPromises = [];
        dashboard.worksheets.forEach(worksheet => dataSourceFetchPromises.push(worksheet.getDataSourcesAsync()));
        const fetchResults = await Promise.all(dataSourceFetchPromises);
    
        // ユニークなデータソースのリストを作成
        const dataSourcesCheck = {};
        const dashboardDataSources = [];
        fetchResults.forEach(dss => {
            dss.forEach(ds => {
                if (!dataSourcesCheck[ds.id]) {
                    // 重複はスキップ
                    dataSourcesCheck[ds.id] = true;
                    dashboardDataSources.push(ds);
                }
            });
        });
    
        // DOM操作
        buildDataSourcesTable(dashboardDataSources);
    });
    
    // データソース名とデータソース更新ボタンを<table>に追加
    function buildDataSourcesTable(dataSources) {
        const dataSourcesTable = $("#dataSourcesTable > tbody")[0];
        for (const dataSource of dataSources) {
            // テーブル行の追加
            const newRow = dataSourcesTable.insertRow(dataSourcesTable.rows.length);
            const nameCell = newRow.insertCell(0);
            const refreshCell = newRow.insertCell(1);
    
            // ボタン要素の作成
            const refreshButton = document.createElement('button');
            refreshButton.innerHTML = ('Refresh Now');
            refreshButton.type = 'button';
            refreshButton.className = 'btn btn-primary';
            refreshButton.addEventListener('click', async () => await dataSource.refreshAsync());
    
            // 行の中身を定義
            nameCell.innerHTML = dataSource.name;
            refreshCell.appendChild(refreshButton);
        }
    }

    TREXファイル

    公式リファレンスに従い、必要事項をXML形式で定義します。
    地味にICONの設定も必須になっています。恐らくはTableau Exchangeでの管理を想定してのこと。
    (中身は70×70のPNGファイルのBase64エンコードです)
    今回ICONは公式サンプルからそのまま持ってきています。

    一番大切なのが「source-location」です。先に作成したHTMLファイル(JSファイル)がどこのサーバでホストされているのかを定義する必要があります。
    今回は私はGithub Pagesでホストしているので、そのURLを記述しています。
    この拡張機能を試してみたいけどホストするのは面倒だな~と思ったら、下記をコピペしたtrexファイルを使ってもらって問題ないです!

    <?xml version="1.0" encoding="utf-8"?>
    <manifest manifest-version="0.1" xmlns="http://www.tableau.com/xml/extension_manifest">
      <dashboard-extension id="com.example.my-extension" extension-version="1.0.0">
        <default-locale>ja_JP</default-locale>
        <name resource-id="name"/>
        <description>シンプルな拡張機能</description>
        <author name="username" email="addr@exmaple.com" organization="org name" website="https://example.com"/>
        <min-api-version>1.10</min-api-version>
        <source-location>
          <url>https://ts-kanzy.github.io/tableau-ext-demo/</url>
        </source-location>
        <icon>iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg==</icon>
        <permissions>
          <permission>full data</permission>
        </permissions>
      </dashboard-extension>
      <resources>
        <resource id="name">
          <text locale="ja_JP">シンプルな拡張機能</text>
        </resource>
      </resources>
    </manifest>

    拡張機能の読み込み

    ダッシュボードに拡張機能オブジェクトを追加し、「ローカル拡張機能にアクセス」から先のtrexファイルを選びます。

     

    データソースの定期的な自動更新を実現するには?

    前回の記事で紹介したデータソースの自動更新は、ボタンが押されたらjsの「setInterval」関数を使って「refreshAsync」APIを定期的に呼び出すことで実現しています。
    実際のコード(TypeScript)の一部は下記のような感じです。

    const iconDOM = this._$("button#refreshTrigger span");
    this._$("button#refreshTrigger").on("click", (e) => {
      // 再生ボタンのクリックイベント
      if(this.intervalRunning){
        // すでに自動更新を実行中だったら停止
        clearInterval(this.intervalInstance);
        iconDOM.removeClass("glyphicon-pause");
        iconDOM.addClass("glyphicon-play");
      }else{
        // 250ミリ秒ごとの自動更新を開始
        this.intervalInstance = setInterval(() => {
          DataSources.refreshAllDataSources(dataSources);
        },250);
        iconDOM.removeClass("glyphicon-play");
        iconDOM.addClass("glyphicon-pause");
      }
      this.intervalRunning = !this.intervalRunning;
    });

    他にはどんなことができる?

    公式リポジトリのSamplesディレクトリが非常に参考になります。
    フィルターやフォーマットの操作等、コードを見てみるのが参考になると思います。

    おわりに

    公式のチュートリアルではTypeScriptやwebpack、その他npmパッケージを使って開発環境を整備していますが、中身の実装自体は結構シンプルだということが伝わっていると嬉しいです!