【ThoughtSpotTips】MuzeStudioで3rdPartyのJavaScriptのライブラリを使って表現する

ThoughtSpotのMuzeStudioで3rdPartyのJavaScriptのライブラリを使って表現する方法をご紹介します

 

AkimasaKajitaniです。以前、MuzeStudio内で、MuzeStudioを使わずに汎用のJavaScriptのライブラリを使ってテーブル形式でデータを表現する方法をご紹介しました。

 

 

今回は改めてMuzeStudioのチャートタイプを使って、汎用的なJavaScriptのライブラリをMuzeStudioで使用する方法をご紹介します。

 

ThoughtSpotのチャートタイプ「Muze Studio」では、Muzeライブラリを使ってチャートを描画できますが、実はJavaScriptのライブラリであればMuzeライブラリを使わずともチャートの描画が可能です。ただ、Geminiなどの生成AIではこのあたり学習していないためかできない、と言い張られてしまうので、教えてあげる必要があります。

MuzeのEditorでは、JS(JavaScript)、CSS、HTMLのタブがあります。このうち主役となるのは「JavaScript」タブとなります。CSSタブは全体的な体裁が調整可能ですし、HTMLタブではテンプレート的にJavaScriptが書き込む部分が記載されています。

デフォルト状態では各タブは以下のようなコードが設定されています。

 

JS(JavaScript)

/**
 * Available Columns:
 * "Month(販売日)"
 * "カテゴリ"
 * "Total 販売額"
 * "Measure names" // If 'measureValues' is enabled.
 * "Measure values" // If 'measureValues' is enabled.
 * --- END --- 
 */

const {
    muze,
    getDataFromSearchQuery
} = viz;

const data = getDataFromSearchQuery();

muze.canvas()
.rows(["Total 販売額"])
.columns(["Month(販売日)"])
.data(data)
.mount("#chart");

JSタブは、データによって、rowsやcolumnsに設定しているカラムや、頭の方にあるコメント化されている部分が変わります。チャートタイプをMuzeStudioにすると、とりあえずデフォルト設定として何かしらのグラフが出るように、データがrowとcolumnsにセットされるようになっています。

 

CSS

html, body {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

#chart {
  width: 100%;
  height: 100%;
}

このタブはどんなデータを詠み込んでも変化はありません。常にこの内容が記載されています。

 

HTML

<div id="chart"></div>

このタブはどんなデータを詠み込んでも変化はありません。常にこの内容が記載されています。

 

Muze Studioのコードをほかのライブラリに適用するには?

 

さて、基本的にMuze以外のライブラリを使うとしても、基本的にはJavaScriptタブの内容のみ編集すればオッケーです(一部のライブラリでは、HTMLタブを編集する必要があります)。Muzeライブラリを使わない場合、使いたいライブラリにどのようにデータを渡すのか、というところがポイントです。

基本的に、流用するのは頭のほうの部分のみですが、デフォルトで記載されているMuze用のコードを見てみましょう。

 

const {
    muze,
    getDataFromSearchQuery
} = viz;

const data = getDataFromSearchQuery();

 

vizオブジェクト(vizと書かれているもの)がThoughtSpotから提供されるオブジェクトです。

muzeはvizオブジェクトの中にあるmuzeライブラリを定義しており、このように呼び出すことでMuzeライブラリを使えるようになります。

また、getDataFromSearchQuery()は、ThoughtSpotの検索した結果からデータモデルインスタンスとしてデータを取得する関数となります。つまり、データの実態がdataに格納されるということになります。

 

そのほかのライブラリを使う場合はこの部分はどうすればよいのでしょうか?

 

const {
	events,
	getDataFromSearchQuery 
} = viz;

const data = getDataFromSearchQuery();
const rawData = data.getData().data;

 

となり、muzeの代わりにeventsを呼び出しています。このeventsを呼び出すことで、ドリルダウンなどのイベントを検知できるようになります。

また、data.getData()を呼び出すことで、ThoughtSpotのデータオブジェクトから純粋なデータ構造を取り出し、最後についている「.data」で利用したいテーブル形式のデータを取り出しています。これは、MuzeライブラリはThoughtSpotのデータ構造を理解できますが、そのほかのライブラリは理解できないため、通常JavaScriptで使われる一般的な生の配列のデータとしてほかのライブラリに渡す必要があるからです。

実際に「getDataFromSearchQuery();」で取得した中身を見てみましょう。これは、「console.log('data', data);」でコンソールに表示が可能です。

 

data:

data {
 "_children": [],
 "_derivations": [],
 "_preDisposeTasks": [],
 "_config": {
  "sanitizeString": false,
  "enableDomainCompletion": true
 },
 "_broadcastChannel": null,
 "_context": {
  "_contextName": "wasm",
  "_fieldOrder": [
   "Month(販売日)",
   "カテゴリ",
   "Total 販売額",
   "__id__"
  ],
  "_fieldMap": {},
  "_dataMeta": {
   "rows": 72,
   "columns": 3
  },
  "_config": {
   "sanitizeString": false,
   "enableDomainCompletion": true
  },
  "_context": {
   "__wbg_ptr": 1114296
  },
  "_partialFieldsDisposed": false
 },
 "_commonDerivation": null,
 "_id": 0,
 "_propagationInfo": {
  "listeners": [],
  "propagationCriterias": {}
 },
 "_refCount": 1
}

 

さらに、「data.getData().data」したrawDataは以下のように表示されます。

 

rawData :

[
 [
  1491004800000,
  "Hardware",
  37766870,
  0
 ],
 [
  1491004800000,
  "Software",
  4547410,
  1
 ],
 [
  1493596800000,
  "Hardware",
  35450380,
  2
 ],
 [
  1493596800000,
  "Software",
  2432090,
  3
 ],
 
 ......途中省略
 
 [
  1583020800000,
  "Hardware",
  83635510,
  71
 ]
]

 

データの並びは、dataの「_fieldOrder」で確認できるとおりです。

 

  "_fieldOrder": [
   "Month(販売日)",
   "カテゴリ",
   "Total 販売額",
   "__id__"
  ],

 

つまり、Month(販売日)、カテゴリ、Total 販売額、__id__という順にデータが入っています。

 

また、日付項目はUNIXタイム(ミリ秒)で取得されているため、実際の取り扱いでは注意する必要があります。

あとは各ライブラリに値を渡し、設定などを行い、最後に描画することでチャートが描画できます(この部分は使用するライブラリで大きく異なるので、ここでは省略しています)。

 

なお、描画が終わった場合、ThoughtSpotに通知するために以下のコードが必要です。

 

events.emitRenderCompletedEvent();

 

Chart.jsを使ったシンプルなサンプル

Chart.jsを使う場合のシンプルなコードは以下の通りになります。Chart.jsの場合は、HTMLタブも修正が必要です。

 

HTMLタブ

<div style="width: 100%; height: 100%;">
    <canvas id="chart"></canvas>
</div>

 

Chart.jsの場合、canvasというタグにチャートを書くようになっています。

 

JSタブ

/**
 * Available Columns:
 * "Month(販売日)"
 * "カテゴリ"
 * "Total 販売額"
 * "Measure names" // If 'measureValues' is enabled.
 * "Measure values" // If 'measureValues' is enabled.
 * --- END --- 
 */

const {
    events,
    getDataFromSearchQuery
} = viz;

// UNIXタイムをYYYY/MM/DDに変換(秒単位・ミリ秒単位の両方に対応)
function formatUnixTimeToDate(unixTime) { 
    if (unixTime === null || unixTime === undefined) return '';
    
    // 数値型に変換
    let time = Number(unixTime);
    
    // UNIXタイムが秒単位(10桁付近)の場合、ミリ秒に変換するために1000をかける
    if (time < 10000000000) {
        time *= 1000;
    }

    const date = new Date(time); 
    const year = date.getFullYear(); 
    const month = String(date.getMonth() + 1).padStart(2, '0'); 
    const day = String(date.getDate()).padStart(2, '0'); 
    return `${year}/${month}/${day}`;
}

// ==========================================
// 1. Chart.jsを動的に読み込む関数
// ==========================================
function loadChartLibrary(callback) {
    // 既に読み込まれている場合は、再読み込みせずに処理を進める
    if (window.Chart) {
        callback();
        return;
    }

    // スクリプトタグを動的に生成
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
    script.async = true;

    // 読み込みが完了したら、後ろに控えている描画処理(callback)を実行する
    script.onload = () => {
        callback();
    };

    // 読み込み失敗時のエラーハンドリング
    script.onerror = () => {
        console.error('Chart.jsの読み込みに失敗しました。');
    };

    // headタグの末尾に追加してロードを開始
    document.head.appendChild(script);
}

// ==========================================
// 2. メインのグラフ描画処理
// ==========================================
function drawMainChart() {
    const data = getDataFromSearchQuery();
    const rawData = data.getData().data;

    // データの前処理(ここで formatUnixTimeToDate を適用し、日付順にソート)
    const months = [...new Set(rawData.map(row => formatUnixTimeToDate(row[0])))].sort();
    const categories = [...new Set(rawData.map(row => row[1]))];

    const datasets = categories.map((category, index) => {
        const colors = [
            'rgba(54, 162, 235, 0.7)', 
            'rgba(255, 99, 132, 0.7)', 
            'rgba(75, 192, 192, 0.7)', 
            'rgba(255, 206, 86, 0.7)'
        ];

        const dataForCategory = months.map(monthLabel => {
            // rawData側のUNIXタイムも変換した上で一致探索を行う
            const found = rawData.find(row => formatUnixTimeToDate(row[0]) === monthLabel && row[1] === category);
            return found ? found[2] : 0;
        });

        return {
            label: category,
            data: dataForCategory,
            backgroundColor: colors[index % colors.length],
            borderColor: colors[index % colors.length].replace('0.7', '1'),
            borderWidth: 1
        };
    });

    // グラフの描画
    const ctx = document.getElementById('chart').getContext('2d');

    // 以前のグラフインスタンスがあれば破棄
    if (window.myChart instanceof Chart) {
        window.myChart.destroy();
    }

    // グラフの作成
    window.myChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: months,
            datasets: datasets
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
                title: { display: true, text: '月別・カテゴリ別 Total 販売額' },
                tooltip: { mode: 'index', intersect: false }
            },
            scales: {
                x: { stacked: true, title: { display: true, text: '販売日 (Month)' } },
                y: { stacked: true, title: { display: true, text: 'Total 販売額 (¥)' }, beginAtZero: true }
            }
        }
    });

    // ThoughtSpot側に描画完了を通知
    events.emitRenderCompletedEvent();
}

// ==========================================
// 3. 実行部分
// ==========================================
// まずライブラリを読み込み、終わったら drawMainChart を実行する
loadChartLibrary(drawMainChart);

 

11-27行の「formatUnixTimeToDate」という関数は、UNIXタイムを標準的な日付形式に変換する部分をです。

 

function formatUnixTimeToDate(unixTime) { 
    if (unixTime === null || unixTime === undefined) return '';
    
    // 数値型に変換
    let time = Number(unixTime);
    
    // UNIXタイムが秒単位(10桁付近)の場合、ミリ秒に変換するために1000をかける
    if (time < 10000000000) {
        time *= 1000;
    }

    const date = new Date(time); 
    const year = date.getFullYear(); 
    const month = String(date.getMonth() + 1).padStart(2, '0'); 
    const day = String(date.getDate()).padStart(2, '0'); 
    return `${year}/${month}/${day}`;
}

 

これにより、以下のようなチャートを書くことができました。

 

その他制限事項

Muze StudioでMuzeライブラリ以外を使用する場合、以下の制限があります。

  • チャート上でThoughtSpotのチャートで出てくる右クリックメニューが出せない
    • これによりできないこと
      • フィルター
      • ドリルダウン
      • ソースデータを表示
      • SpotIQ分析
  • XLSXダウンロードは別途コードが必要
    • コードを書けば対応可能

 

一方で、ライブボードフィルタなどは、データソース側に効いてくるため機能します。

また、ほかのThoughtSpot純正のチャートで右クリックからフィルターをかける場合は適用されるようになっています。

あくまで、3rdパーティのライブラリを使って表現したチャートから他のチャートに対してのフィルタができない、というお話です。

 

PNG/PDF/XLSXダウンロード

実は、MuzeStudio以外のライブラリを使う場合、XLSX形式でのダウンロードを有効にするにはコードを追加する必要があります。また、PDF/PNG形式のダウンロードの場合、アニメーションをオフにすることが推奨されています。

ただ、実際にやってみたところ、PDF/PNGでのダウンロードは特に対策コードを入れなくても動いていました(サンプルコードのD3.jsを使ったものでさえ、PDF/PNGダウンロード対策部分を削除しても問題ありませんでした)。ただ、うまくいかない場合はサンプルコードをもとに対策コードを入れてみてください。

XLSX形式については必要なコードが書かれていないため、まったくぴくりとも反応しないです。必要な場合はコードを追加する必要があります。

 

参考

Charts Built with Third-Party Libraries | ThoughtSpot Developers Documentation

 

※ThoughtSpot バージョン 26.7.0.cl-86時点の情報です

おすすめの記事