投稿者:中根 洋平

Looker Advent Calendar 2019 - Qiitaの16日目の記事です。
今回はLookerのCustom Visualizationについて紹介したいと思います。

Custom Visualizationとは

Lookerには標準で色々なVisualizationが用意されていますがユーザにて作成できるのがCustom Visualizationです。

アドベントカレンダー6日目のymtoさんの記事にもあったサンキーダイアグラムLookerでサンキーダイアグラムの可視化手順をまとめてみた - Qiitaや、
img-001.jpg
ツリーマップなどがGitHub - looker/custom_visualizations_v2に用意されています。
img-002.jpg

もちろんここまで複雑なものではなく、すこしだけカスタマイズしたい時、エンベデッド作成するほどではないんだけど、という時にも重宝します。

Custom Visualizationの有効化

有効にするにはAdmin権限が必要となります。
Admin Settings - Visualizationsに手順はありますが大きく以下の3ステップで表示が可能となります。  

  1. Admin-General-LabsからSandboxed Custom Visualizationsを有効にする。
  2. Admin-Platform-VisualizationのAdd Visualizationをクリック
  3. MainにJavaScriptのパスを指定。

作ってみよう

Custom Visualization自体はJavaScriptで作成します。またReactにも対応しています。
Githubにスタートガイドがあるのでこちらを参考にしてみると良いでしょう。

今回はスタートガイドのHello Worldを元にwojtekmaj/react-calendarを使用してLookerにカレンダーを表示してみます。

基本的にはHello Worldを元にReact-calendarのサンプルを組み込んでいきます。

import React from 'react'
import ReactDOM from 'react-dom';
import Calendar from 'react-calendar';

class LookerCalendar extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            activeStartDate: props.activeStartDate,
            events: props.events,
            font_size: props.font_size
        };
        this.getTileClass = this.getTileClass.bind(this);
        this.getTileContent = this.getTileContent.bind(this);
    }

    // Formatter
    getFormatDate(date) {
        return `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}`;
    }

    // Holiday and Weekend
    getTileClass({ date, view }) {
        // Only Month view
        if (view !== 'month') {
            return '';
        }
        const day = this.getFormatDate(date);
        if (this.state.events[day] && this.state.events[day].holiday === 'Yes') {
            return 'holiday'
        } else if (this.state.events[day] && this.state.events[day].weekday === 0) {
            return 'sunday'
        } else if (this.state.events[day] && this.state.events[day].weekday === 6) {
            return 'saturday'
        } else {
            return ''
        }
    }

    // Event
    getTileContent({ date, view }) {
        // Only Month view
        if (view !== 'month') {
            return null;
        }
        const day = this.getFormatDate(date);
        // If there is content, display
        return (
            <p style={{ fontSize: this.props.font_size }}>
                {(this.state.events[day] && this.state.events[day].text) ?
                    this.state.events[day].text : <br></br>
                }
            </p>
        );
    }

    render() {
        return (
            <div>
                <Calendar
                    tileClassName={this.getTileClass}
                    tileContent={this.getTileContent}
                    activeStartDate={this.props.activeStartDate}
                />
            </div>
        );
    }
}

looker.plugins.visualizations.add({
    // Id and Label are legacy properties that no longer have any function besides documenting
    // what the visualization used to have. The properties are now set via the manifest
    // form within the admin/visualizations page of Looker
    id: "calendar",
    label: "Calendar",
    options: {
        font_size: {
            type: "string",
            label: "Font Size",
            values: [
                { "Large": "large" },
                { "Small": "small" }
            ],
            display: "radio",
            default: "large"
        }
    },
    // Set up the initial state of the visualization
    create: function (element, config) {

        // Insert a <style> tag with some styles we'll use later.
        element.innerHTML = `
            <style>
                .react-calendar__month-view__weekdays__weekday abbr{text-decoration: none;}
                .holiday{ color: #ff0000;}
                .sunday{ color: #ff0000;}
                .saturday{ color: #0000FF;}
                }
            </style>
            `;

        // Create a container element to let us center the text.
        let container = element.appendChild(document.createElement("div"));
        container.className = "calendar";
        this._element = container.appendChild(document.createElement("div"));
    },
    // Render in response to the data or settings changing
    updateAsync: function (data, element, config, queryResponse, details, done) {

        // Clear any errors from previous updates
        this.clearErrors();

        // Throw some errors and exit if the shape of the data isn't what this chart needs
        if (queryResponse.fields.dimensions.length == 0) {
            this.addError({ title: "No Dimensions", message: "This chart requires dimensions." });
            return;
        }
        // Set the size
        let font_size = config.font_size || this.options.font_size.default;
        if (config.font_size == "small") {
            font_size = "12px";
        } else {
            font_size = "24px";

        }

        // Parse Event
        let events = {}
        let days = []
        for (var row of data) {
            let date_cell = row[queryResponse.fields.dimensions[0].name].value;
            let content_cell = row[queryResponse.fields.dimensions[1].name].value;
            let holiday_cell = row[queryResponse.fields.dimensions[2].name].value;
            // Parse Date object
            const date = new Date(date_cell);
            events[date_cell] = {
                text: content_cell,
                weekday: date.getDay(),
                holiday: holiday_cell
            };
            days.push(date);
        };
        const max_date = new Date(Math.max.apply(null, days));
        const start_date = new Date(max_date.setDate(max_date.getDate() - 30))
        
        // Finally update the state with our new data
        this.chart = ReactDOM.render(
            <LookerCalendar
                activeStartDate={start_date}
                events={events}
                font_size={font_size}
            />, this._element
        );

        // We are done rendering! Let Looker know.
        done();
    }
});

上記ソースをyarnでビルド、dist配下に出力されたファイルをアップロードしましょう。
アップロードしたファイルをパスをAdmin-Platform-Visualizationから登録しましよう。
うまく指定できていればVisualizationsの中に作成したラベルが表示されています。

このCustom Visualizationに以下のようなクエリ結果を与えた時にカレンダーを表示します。

  • 1列目: 日付
  • 2列目: テキスト
  • 3列目: 祝日フラグ(Boolean)

また、Custom Visualizationでもオプションが設定ができます。
今回は文字サイズをラジオボタンで選択できるようにしてみました。
*フォントの更新は再描画のタイミングですが。。。

img-003.jpg

今回の例ではテキストをアイコンで表示しています。
小売店の店員のようにダッシュボードをじっくり確認することが難しいユーザに対して売り上げの達成率をアイコンを使用して表示するといった使い方をすることで直感的に情報を把握できるようになるのではと思います。  

ほかにも

以下の画面は弊社が今年のNext Tokyoで作成したデモ用ダッシュボードの一部です。
車毎の走行情報を可視化しています。
標準の地図でも2地点間をつなげて表示ができますが、細かな移動経路の表示ができなかったのでBigQueryのGIS関数と組み合わせて移動経路の表示を可能としています。

img-004.jpg

まとめ

今回Custom Visualizationを利用してみましたがJavaScriptを使用できるので非常に強力です。

ただ、なんでもかんでもCustom Visualizationを利用するかどうかは気をつけなければいけない点が多いです。
まずはパフォーマンス。処理の方法によっては表示にかなり時間がかかることがあります。
次にサポート。Custom Visualizationはコミュニティサポートとなります。つまり、何か不具合が会った時はLookerのサポートへチャットすることはできません。

私個人としては上記のことからより複雑なVIsualizationが必要になった場合はAPIを用いたアプリを作成した方が良い場合が多いのかなという印象です。
ただ、先日発表のあったLoker7ではCustom Visualizationの強化や、マーケットプレイスといった発表があったのでこれから変わっていくと思います。