初心者のためのExtJS入門

ExtJSを使うので、ついでにまとめていきます

(ExtJS6.5)グリッドの新機能[modern] (1)

ExtJS6.5でmodernのグリッドにいくつか変更箇所があります。

今回は選択と編集を試してみました。

選択

Ext.grid.Gridのselectableコンフィグを使って、どういう選択を可能にするかを設定できるようになりました。

items: [{
    xtype: 'grid',

    selectable: {
        checkbox: true,
        cells: true
    }
}]

プラグインで指定していた以前より、可読性が良くなりました。

ドキュメントを見る限り8つの項目を指定できるようです。

- mode `'single'`, `'multi'` Allow selection of only a single or multiple *records*. This is only valid when selecting rows.
- deselectable Configure as false to disallow deselecting down to zero selected *records*. This is only valid when selecting rows.
- drag `true` or `false` to allow drag gestures to swipt a rage of cells or rows.
- columns `true` to enable column selection by clicking on headers. Defaults to `false`
- cells `true` to enable cell selection by clicking or dragging on cells. Defaults to `false`
- rows Set to `false` to disable selecting rows. Defaults to `true`
- checkbox `true` to add a checkbox column to display selected state. `'only'` to indicate that only clicks on the checkbox affect row selected state.
- extensible `true` to enable the selection to be extended either in the `X` or `Y` axis or `'x'` or `'y'` to configure

グリッドの場合は、Ext.grid.selection.Model(http://docs.sencha.com/extjs/6.5.0/modern/Ext.grid.selection.Model.html)というセレクションモデルのクラスを使っているようで、このクラスのコンフィグとして渡してくれるみたいです。

編集

Ext.grid.plugin.CellEditingプラグインクラスを使って、セル単位で編集できるようになりました。classicでは当たり前にできていましたが、modernでは新規です。

これまでの編集は、行を選択すると、編集フォームが画面外からスライドイン表示され、そこで編集作業を行うという流れでした。

しかし、PCで扱う場合は直接編集できたほうが操作感が良いため、PC向けに作成されたようです。

このことから、最終的にはPC・モバイルともにmodernにまとめたいという意思を感じます。

PC向けの場合は新しく追加されたプラグインを、モバイル向けの場合はこれまでのUIを使いたい場合は、platformConfigで設定するコンフィグを切り替えできるようです。

items: [{
    xtype: 'grid',

    platformConfig: {

        desktop: {
            plugins: {
                gridcellediting: true
            }
        },

        '!desktop': {
            plugins: {
                grideditable: true
            }
        }
    },
    ...]
}]

試してみると、PCのユーザエージェントだと↓のようにセル単位でのインライン編集となり

f:id:sham-memo:20170530223417p:plain

タブレットのユーザエージェントだと↓のように編集フォームでの編集操作となりました。

f:id:sham-memo:20170530223430p:plain

インライン編集は、編集中にグリッドをスクロールしても編集の行はその位置に固定された状態になるようです。なるほど、スーパーパワー!(意味が分からない人はドキュメントをお読みください)

f:id:sham-memo:20170530224750p:plain

アプリケーションで使用するExtJSのバージョンを上げる

ExtJS6.5がリリースされましたね。

https://www.sencha.com/blog/announcing-ext-js-6-5-and-sencha-cmd-6-5-ga/

いろいろ変更ありますが、modern向け機能の強化がうれしいです。

早速SDKとSenchaコマンドをダウンロードしたので、アプリケーションで使用するExtJSのバージョンを6.5に上げてみます。

> cd (アプリケーションのパス)
> sencha app upgrade (ExtJS6.5 SDKのパス)

Sencha Cmd v6.5.0.180
[INF] Upgrading framework ext
[INF] Copying framework to (アプリケーションのパス)/ext
[INF] Framework 'ext' upgraded

workspace.jsonを見ると↓のように6.5になりました。

"frameworks": {
    "ext": {
        "path":"ext",
        "version":"6.5.0.775"
    }
}

更新後は.senchaディレクトリのファイルがごそっと削除されています。cfgやxmlファイルをカスタマイズしてた場合は影響を受けそうですね。6.5以降は、.senchaディレクトリを使わなくなるのかな?

app.jsonのrequiresで、modernに存在しないext-localeを定義していたらsencha app buildでエラーとなるようになっていました。

requiresはclassicとmodernで分けておいたほうが良さそうです。

 

 

あとはES6サポートを試してみました。class Panel {} みたいに書けるのかと思っていましたが、まだそこまでの対応ではないようです。

PromiseのようなES6の機能を使って実装した場合、ビルドしたときにサポートしていないブラウザでも動作するように出力してくれるだけみたいです。

おそらくExtJS7で構文が変わるんでしょうね。

今後、他の機能をちょこちょこ試していこうと思います。

あ、以降の投稿ではExtJS6.5を使うつもりです。

ダッシュボード[classic]

Ext.dashboard.Dashboardを使うと、業務系の画面でよくあるダッシュボードのUIを実現できます。

partsコンフィグに、適当なキー名でExt.dashboard.Partクラスのコンフィグを指定します。

さらにExt.dashboard.PartのviewTemplateコンフィグのitemsに表示したいビューを指定することになります。

まだpartsコンフィグだけでは表示されないです。

defaultContentコンフィグには、最低限typeとcolumnIndexを指定したオブジェクトリテラルの配列を指定します。

ここでtypeはpartsコンフィグで指定したキー名、columnIndexは列のインデックス番号(0から)です。

/**
 * ダッシュボードパネルクラス。
 *
 * @class Sample.view.main.dashboard.Panel
 * @extend Ext.dashboard.Dashboard
 */
Ext.define('Sample.view.main.dashboard.Panel', {
    extend: 'Ext.dashboard.Dashboard',
    xtype: 'main_dashboard_panel',

    parts: {
        testKey: {
            viewTemplate: {
                items: {
                    xtype: 'panel',
                    html: 'パネル'
                }
            }
        }
    },

    defaultContent: [
        {
            type: 'testKey',
            columnIndex: 0
        },
        {
            type: 'testKey',
            columnIndex: 1
        },
        {
            type: 'testKey',
            columnIndex: 2
        },
        {
            type: 'testKey',
            columnIndex: 1
        }
    ]
});

f:id:sham-memo:20170507232637p:plain

上記だと、testKeyのビューを4つダッシュボードに表示しています。columnIndex=1を2つ指定しているので、2列目のビューは2つになっています。

f:id:sham-memo:20170508004633p:plain

ドラッグ&ドロップ操作で場所を移動させることができます。

動的にビューを追加する

ボタンクリック時にビューを追加してみます。

/**
 * ダッシュボードパネルクラス。
 *
 * @class Sample.view.main.dashboard.Panel
 * @extend Ext.dashboard.Dashboard
 */
Ext.define('Sample.view.main.dashboard.Panel', {
    extend: 'Ext.dashboard.Dashboard',
    xtype: 'main_dashboard_panel',

    parts: {
        testKey: {
            viewTemplate: {
                items: {
                    xtype: 'panel',
                    html: 'パネル'
                }
            }
        }
    }
});

/**
 * メインパネル。
 *
 * @class Sample.view.main.Panel
 * @extend Ext.panel.Panel
 */
Ext.define('Sample.view.main.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'main_panel',

    requires: [
        'Sample.view.main.dashboard.Panel',
        'Sample.view.main.ViewController'
    ],

    controller: 'main',

    cls: 'main-panel',

    layout: 'fit',

    dockedItems: {
        xtype: 'toolbar',
        items: [
            '->',
            {
                text: '追加',
                handler: 'onClickAddButton'
            }
        ]
    },

    items: {
        reference: 'dashboard_panel',
        xtype: 'main_dashboard_panel'
    }
});

/**
 * ビューコントローラクラス。
 *
 * @class Sample.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Sample.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.main',

    onClickAddButton: function () {
        var me = this,
            dashboardPanel = me.lookupReference('dashboard_panel');

        dashboardPanel.addNew('testKey', 0);
    }
});

Ext.dashboard.DashboardのaddNewメソッドで追加できます。第1引数はpartsコンフィグに定義したキー名、第2引数は列のインデックス番号です。

でも、これだとpartsコンフィグに定義されていない呼び出せないです。なので、partsコンフィグにも動的に追加してみます。

/**
 * ダッシュボードパネルクラス。
 *
 * @class Sample.view.main.dashboard.Panel
 * @extend Ext.dashboard.Dashboard
 */
Ext.define('Sample.view.main.dashboard.Panel', {
    extend: 'Ext.dashboard.Dashboard',
    xtype: 'main_dashboard_panel',

    columnWidths: [
        0.5,
        0.5
    ]
});

/**
 * メインパネル。
 *
 * @class Sample.view.main.Panel
 * @extend Ext.panel.Panel
 */
Ext.define('Sample.view.main.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'main_panel',

    requires: [
        'Sample.view.main.dashboard.Panel',
        'Sample.view.main.ViewController'
    ],

    controller: 'main',

    cls: 'main-panel',

    layout: 'fit',

    dockedItems: {
        xtype: 'toolbar',
        items: [
            '->',
            {
                text: '列1に追加',
                handler: 'onClickAddColumn1Button'
            },
            {
                text: '列2に追加',
                handler: 'onClickAddColumn2Button'
            }
        ]
    },

    items: {
        reference: 'dashboard_panel',
        xtype: 'main_dashboard_panel'
    }
});

/**
 * ビューコントローラクラス。
 *
 * @class Sample.view.main.ViewController
 * @extend Ext.app.ViewController
 */
Ext.define('Sample.view.main.ViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.main',

    /**
     * 列1に追加ボタンクリック時の処理。
     */
    onClickAddColumn1Button: function () {
        var me = this;
        
        // 第1,2引数は、応用を考えて仮の値を固定値で渡している。
        // 実際には、他の入力フィールドの値やサーバからのレスポンスデータを渡すことが考えられる。
        me.addComponent(me.getContentId(), 'panel', 0);
    },

    /**
     * 列2に追加ボタンクリック時の処理。
     */
    onClickAddColumn2Button: function () {
        var me = this;
        me.addComponent(me.getContentId(), 'panel', 1);
    },

    /**
     * コンポーネントを追加する。
     * 
     * @param {Number} id ID
     * @param {String} type ビューの種類
     * @param {Number} columnIndex 列インデックス番号
     */
    addComponent: function (id, type, columnIndex) {
        var me = this,
            dashboardPanel = me.lookupReference('dashboard_panel'),
            partKey = me.getPartKey(id, type),
            parts = dashboardPanel.getParts();

        dashboardPanel.setParts(me.getPartConfig(id, type), parts);

        dashboardPanel.addNew(partKey, columnIndex);
    },

    /**
     * Ext.dashboard.Partのコンフィグを返す。
     * 
     * @param {Number} id ID
     * @param {String} type ビューの種類
     * @returns {Object} コンフィグ
     */
    getPartConfig: function (id, type) {
        var me = this,
            partKey = me.getPartKey(id, type),
            config = {};

        config[partKey] = {
            viewTemplate: {
                items: me.getPartItemConfig(id, type)
            }
        };

        return config;
    },

    /**
     * Ext.dashboard.Partに設定するアイテムコンポーネントのコンフィグを返す。
     * 
     * @param {Number} id ID
     * @param {String} type ビューの種類
     * @returns {Object} コンフィグ
     */
    getPartItemConfig: function (id, type) {
        // MEMO: 実案件などに使う場合は、おそらくtypeに応じて返すコンフィグを切り替えたりすることになる
        return {
            xtype: type,
            html: 'content ' + id
        };
    },

    /**
     * partsコンフィグのキーを返す。
     * 
     * @param {Number} id ID
     * @param {String} type ビューの種類
     * @returns {String} キー
     */
    getPartKey: function (id, type) {
        return type + id;
    },

    /**
     * IDを生成する。
     * @returns {number} ID
     */
    getContentId: function () {
        return Math.floor(Math.random() * 100);
    }
});

少し応用などを考えた実装になっていますが、やりたかったことの実装部分はaddComponentメソッドです。

注意としては、Ext.dashboard.DashboardクラスのcolumnWidthsコンフィグを指定しておくことです。

columnWidthsコンフィグは列の幅の割合を、1を分け合う値で指定するのですが、これを指定していないとおかしな挙動になります。

 

 

 

ビューの削除やドロップのイベントについても取り上げようと思いましたが、Ext.dashboardパッケージのクラスのソースを見ればわかりそうなので今回は割愛です。

本番用のapp.jsでrequireの設定不足によるJSエラーが発生する場合の対処

ExtJSで良くあるのが、requireの記述漏れです。

本番用にsencha app buildで出力したファイルで動かしたら、↓のようなJSエラーが発生。この場合は、まずrequiresにクラス指定が漏れています。

Uncaught TypeError: c is not a constructor
    at eval (eval at getInstantiator (app.js:1), <anonymous>:3:8)
    at Object.create (app.js:1)
    at Ext.Inventory.instantiateByAlias (app.js:1)
    at ai.create (app.js:1)
    at ai.constructPlugin (app.js:1)
    at ai.constructPlugins (app.js:1)
    at ai.initComponent (app.js:1)
    at ai.initComponent (app.js:1)
    at ai.initComponent (app.js:1)
    at ai.initComponent (app.js:1)

ブラウザコンソール上に警告が出ていないか確認する

sencha app watchで動かしている場合、下記のような警告を出してくれることがあるので、この場合は速やかにrequiresに追記しましょう。

[W] [Ext.Loader] Synchronously loading 'Ext.chart.series.Line'; consider adding Ext.require('Ext.chart.series.Line') above Ext.onReady

Ext.ClassManagerのinstantiateByAliasメソッドで確認する

しかし、時々、警告が出ないパターンがあります。

その場合は、Ext.ClassManagerのinstantiateByAliasメソッド上にどうにかコンソールログなどを加えて直接見るしかありません。

画面表示時にいきなりエラーが出ないのであれば、エラー表示前に↓をブラウザコンソールで実行して、コンソールログ出力を加えます。

var originFunc = Ext.ClassManager.instantiateByAlias;

Ext.ClassManager.instantiateByAlias = function () {
  console.log(arguments);
  return originFunc.apply(this, arguments);
}

この状態でrequire不足のJSエラーを発生させれば、どのクラスが足りていないのかログ出力されます。

ブラウザでの初期画面表示時にエラーが出る場合は、ブラウザコンソールで実行しても間に合わないので、Application.jsのlaunchメソッドなどに上記コードを追加してしまうか直接ExtJSの該当コードにコンソールログを埋め込むなどの工夫が必要でしょう。Ext.ClassManager自体がシングルトンなので、overridesではオーバーライドはできません。

チャート[classic] (3)

スタック形式の棒グラフ

Ext.chart.series.Barのstackedコンフィグをtrueにすると、積み上げた形状の棒グラフを表現できます。

これを使うとデータを比較しやすくなります。

モデル、ストア

車の販売台数のデータでモデル、ストアを作成しました。(データ元: http://www.jada.or.jp/contents/data/hanbai/maker.html)

/**
 * 車販売台数モデルクラス。
 *
 * @class Sample.model.CarSales
 * @extend Ext.data.Model
 */
Ext.define('Sample.model.CarSales', {
    extend: 'Ext.data.Model',

    fields: [
        {
            name: 'ym',
            type: 'string'
        },
        {
            name: 'data1',
            type: 'int'
        },
        {
            name: 'data2',
            type: 'int'
        },
        {
            name: 'data3',
            type: 'int'
        },
        {
            name: 'data4',
            type: 'int'
        },
        {
            name: 'data5',
            type: 'int'
        },
        {
            name: 'data6',
            type: 'int'
        },
        {
            name: 'data7',
            type: 'int'
        },
        {
            name: 'data8',
            type: 'int'
        },
        {
            name: 'data9',
            type: 'int'
        },
        {
            name: 'data10',
            type: 'int'
        },
        {
            name: 'data11',
            type: 'int'
        },
        {
            name: 'data12',
            type: 'int'
        },
        {
            name: 'data13',
            type: 'int'
        }
    ]
});

/**
 * 車販売台数ストアクラス。
 *
 * @class Sample.store.CarSales
 * @extend Ext.data.Store
 */
Ext.define('Sample.store.CarSales', {
    extend: 'Ext.data.Store',

    model: [
        'Sample.model.CarSales'
    ],

    model: 'Sample.model.CarSales',

    proxy: 'memory',

    data: [
        { ym: '2016/04', data1: 721, data2: 7885, data3: 3681, data4: 23993, data5: 4184, data6: 9441, data7: 1879, data8: 2620, data9: 19177, data8: 2620, data9: 19177, data10: 7871, data11: 111693, data12: 670, data13: 18898 },
        { ym: '2016/05', data1: 430, data2: 8243, data3: 4146, data4: 27949, data5: 4967, data6: 10484, data7: 1510, data8: 3082, data9: 24690, data8: 3082, data9: 24690, data10: 7353, data11: 105528, data12: 647, data13: 24724 },
        { ym: '2016/06', data1: 491, data2: 8870, data3: 5754, data4: 33944, data5: 6549, data6: 10478, data7: 1876, data8: 4897, data9: 30561, data8: 4897, data9: 30561, data10: 9239, data11: 138878, data12: 1034, data13: 35199 },
        { ym: '2016/07', data1: 676, data2: 8906, data3: 4622, data4: 31698, data5: 6716, data6: 13431, data7: 2279, data8: 3313, data9: 27747, data8: 3313, data9: 27747, data10: 8647, data11: 146536, data12: 844, data13: 26338 },
        { ym: '2016/08', data1: 854, data2: 8367, data3: 4623, data4: 22381, data5: 6481, data6: 12195, data7: 1295, data8: 3316, data9: 22466, data8: 3316, data9: 22466, data10: 6438, data11: 110527, data12: 799, data13: 23531 },
        { ym: '2016/09', data1: 657, data2: 11145, data3: 7168, data4: 38064, data5: 10833, data6: 18112, data7: 1505, data8: 4936, data9: 31573, data8: 4936, data9: 31573, data10: 8781, data11: 144012, data12: 1071, data13: 39191 },
        { ym: '2016/10', data1: 538, data2: 9249, data3: 4527, data4: 32629, data5: 5382, data6: 10227, data7: 2279, data8: 2913, data9: 25695, data8: 2913, data9: 25695, data10: 6457, data11: 118712, data12: 791, data13: 23470 },
        { ym: '2016/11', data1: 994, data2: 11832, data3: 5500, data4: 31681, data5: 6902, data6: 13537, data7: 2242, data8: 3426, data9: 34997, data8: 3426, data9: 34997, data10: 6892, data11: 126837, data12: 917, data13: 27285 },
        { ym: '2016/12', data1: 1106, data2: 10269, data3: 5688, data4: 28676, data5: 7501, data6: 8564, data7: 1954, data8: 4625, data9: 31751, data8: 4625, data9: 31751, data10: 5919, data11: 124348, data12: 1045, data13: 33492 },
        { ym: '2017/01', data1: 1123, data2: 12722, data3: 4380, data4: 30338, data5: 5103, data6: 13946, data7: 2301, data8: 2352, data9: 40324, data8: 2352, data9: 40324, data10: 8828, data11: 114916, data12: 577, data13: 21175 },
        { ym: '2017/02', data1: 975, data2: 13218, data3: 5284, data4: 34267, data5: 6824, data6: 16027, data7: 2596, data8: 3278, data9: 44952, data8: 3278, data9: 44952, data10: 9183, data11: 147714, data12: 699, data13: 27018 },
        { ym: '2017/03', data1: 1819, data2: 18496, data3: 11443, data4: 49847, data5: 13160, data6: 28751, data7: 5494, data8: 5878, data9: 64395, data8: 5878, data9: 64395, data10: 12834, data11: 200587, data12: 1366, data13: 46584 }
    ]

});

チャート

あとはExt.chart.series.Barを使って棒グラフを表示してみます。

積み上げる順番にyFieldに複数のフィールド名を指定します。

あとはstacked: trueを指定します。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.series.Bar'
    ],

    store: 'CarSales',

    legend: {
        type: 'sprite',
        docked: 'bottom'
    },

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '販売台数'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            label: {
                rotate: {
                    degrees: -90
                }
            },
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar',
            xField: 'ym',
            yField: ['data1','data2','data3','data4','data5','data6','data7','data8','data9','data10','data11','data12','data13'],
            title: ['ダイハツ','富士重工','日野','ホンダ','いすゞ','マツダ','三菱','三菱ふそう','日産','スズキ','トヨタ','UDトラックス','輸入車'],
            stacked: true,
            style: {
                minGapWidth: 20
            }
        }
    ]
});

f:id:sham-memo:20170427203121p:plain

legendコンフィグで、Excelの凡例みたいなのを表示できます。複数のデータがあると、どの色が何を表しているのか分からないので、この形式だと必須ですね。

スタック形式の割合で表示

さらに自動的に各データの全体での割合のチャートにすることができます。

fullStack: trueを指定するだけです。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.series.Bar'
    ],

    store: 'CarSales',

    legend: {
        type: 'sprite',
        docked: 'bottom'
    },

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '販売台数の割合(%)'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            label: {
                rotate: {
                    degrees: -90
                }
            },
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar',
            xField: 'ym',
            yField: ['data1','data2','data3','data4','data5','data6','data7','data8','data9','data10','data11','data12','data13'],
            title: ['ダイハツ','富士重工','日野','ホンダ','いすゞ','マツダ','三菱','三菱ふそう','日産','スズキ','トヨタ','UDトラックス','輸入車'],
            stacked: true,
            fullStack: true,
            style: {
                minGapWidth: 20
            }
        }
    ]
});

f:id:sham-memo:20170427203336p:plain

各年月で、全体を100%としたときの割合を表示しています。

データを横に並べる

データを横並びにして比べるなら、stack: falseを指定します(stackコンフィグの初期値はtrueなんですね。知らなかった)。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.series.Bar'
    ],

    store: 'CarSales',

    legend: {
        type: 'sprite',
        docked: 'bottom'
    },

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '販売台数'
            },
            grid: {
                odd: {
                    fillStyle: 'rgba(255, 255, 255, 0.06)'
                },
                even: {
                    fillStyle: 'rgba(0, 0, 0, 0.03)'
                }
            }
        },
        {
            type: 'category',
            position: 'bottom',
            label: {
                rotate: {
                    degrees: -90
                }
            },
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar',
            xField: 'ym',
            yField: ['data11', 'data4', 'data1'],
            stacked: false,
            style: {
                inGroupGapWidth: -7
            },
            title: ['トヨタ', 'ホンダ', 'ダイハツ']
        }
    ]
});

f:id:sham-memo:20170429151744p:plain

立体的な棒グラフ

軸、データ系列のクラス名が「~3D」となっているものを使用します。その他の設定は、これまでとほぼ同じです。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric3D',
        'Ext.chart.axis.Category3D',
        'Ext.chart.series.Bar3D',
        'Ext.chart.grid.HorizontalGrid3D'
    ],

    store: 'CarSales',

    legend: {
        type: 'sprite',
        docked: 'bottom'
    },

    axes: [
        {
            type: 'numeric3d',
            position: 'left',
            minimum: 0,
            title: {
                text: '販売台数'
            },
            grid: {
                odd: {
                    fillStyle: 'rgba(255, 255, 255, 0.06)'
                },
                even: {
                    fillStyle: 'rgba(0, 0, 0, 0.03)'
                }
            }
        },
        {
            type: 'category3d',
            position: 'bottom',
            label: {
                rotate: {
                    degrees: -90
                }
            },
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar3d',
            xField: 'ym',
            yField: ['data11', 'data4', 'data1'],
            stacked: false,
            style: {
                inGroupGapWidth: -7
            },
            title: ['トヨタ', 'ホンダ', 'ダイハツ']
        }
    ]
});

f:id:sham-memo:20170429152018p:plain

チャート[classic] (2)

今回はclassicのチャートの続きです。

いくつかチャートの種類を試してみます。

Ext.chart.series.Line

折れ線グラフです。

insetPaddingやinnerPaddingでチャートのパディングを調整しています。何らかの方法で表示を調整しないと、テキストがチャート外にはみ出すことがあるので注意が必要です。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.interactions.ItemHighlight',
        'Ext.chart.series.Line'
    ],

    store: 'WaterStorage',

    insetPadding: {
        top: 40,
        bottom: 40,
        left: 20,
        right: 40
    },

    innerPadding: {
        top: 20,
        left: 40,
        right: 40
    },

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '貯水量'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'line',
            xField: 'ym',
            yField: 'amount',
            style: {
                stroke: '#ad5987',
                lineWidth: 2
            },
            marker: {
                type: 'circle',
                radius: 4,
                lineWidth: 2,
                fill: '#ad5987'
            },
            label: {
                field: 'amount',
                display: 'under'
            }
        }
    ]
});

f:id:sham-memo:20170423133225p:plain

Ext.chart.series.Bar

棒グラフです。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    store: 'WaterStorage',

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '貯水量'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar',
            xField: 'ym',
            yField: 'amount',
            style: {
                minGapWidth: 20,
                stroke: '#50ada1',
                fill: '#50ada1'
            },
            highlight: {
                strokeStyle: '#4e7d9c',
                fillStyle: '#4e7d9c'
            },
            label: {
                field: 'amount',
                display: 'insideEnd'
            }
        }
    ]

});

f:id:sham-memo:20170423133239p:plain

↓のように水平方向にもできます。

Ext.chart.CartesianChartのflipXYコンフィグをtrueで指定し、軸(axes)の位置(position)を入れ替えています。

一応理屈は同じなので、Ext.chart.series.Lineなどでも可能です(需要はなさそうですが)。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.interactions.ItemHighlight',
        'Ext.chart.series.Bar'
    ],

    store: 'WaterStorage',

    flipXY: true,

    axes: [
        {
            type: 'numeric',
            position: 'bottom',
            minimum: 0,
            title: {
                text: '貯水量'
            }
        },
        {
            type: 'category',
            position: 'left',
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'bar',
            xField: 'ym',
            yField: 'amount',
            style: {
                stroke: '#50ada1',
                fill: '#50ada1'
            },
            highlight: {
                strokeStyle: '#4e7d9c',
                fillStyle: '#4e7d9c'
            },
            label: {
                field: 'amount',
                display: 'insideEnd'
            }
        }
    ]
});

f:id:sham-memo:20170423140730p:plain

Ext.chart.series.Area

面グラフです。

highlightCfgコンフィグで、マウスがマーカーに近づいたときにマーカーのスタイルを変更できます。

あとtooltipコンフィグで、マウスがマーカーに近づいたときにツールチップを表示させています。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.interactions.ItemHighlight',
        'Ext.chart.series.Area'
    ],

    store: 'WaterStorage',

    axes: [
        {
            type: 'numeric',
            position: 'left',
            grid: true,
            minimum: 0,
            title: {
                text: '貯水量'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            label: {
                rotate: {
                    degrees: -90
                }
            },
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'area',
            xField: 'ym',
            yField: 'amount',
            style: {
                stroke: '#ad5987',
                fill: '#d9aac4',
                opacity: 0.6,
                lineWidth: 1
            },
            marker: {
                opacity: 0,
                scaling: 0,
                fx: {
                    duration: 200,
                    easing: 'easeOut'
                }
            },
            highlightCfg: {
                opacity: 1,
                scaling: 1.5
            },
            tooltip: {
                trackMouse: true,
                renderer: function (tooltip, record, item) {
                    tooltip.setHtml(record.get('ym') + ' : ' + record.get('amount'));
                }
            }
        }
    ]
});

f:id:sham-memo:20170423140741p:plain

とりあえず代表的なグラフを取り上げました。

まだいくつも種類があるので、もう少しチャート回が続きます。

チャート[classic] (1)

今回はclassicのチャートです。

まずはチャートを使うための基本を押さえておきます。

app.json

チャートの機能は別パッケージになっているので、app.jsonのrequiresにチャートのパッケージ名chartsを指定します。

"requires": [
    "charts"
]

実装イメージ

Ext.chartパッケージに~Chartというクラスがいくつか存在します。

チャートを表示するときは、軸(axesコンフィグ)とデータ系列(seriesコンフィグ)をそれらクラスに指定します。

この辺りは、数学で最初のころにグラフを覚えたときと同じです。

まずx, y軸の線を引いて、それからデータをプロットし、線グラフにしたり棒グラフにしたりといった感じでグラフを書いていくことでしょう。

ExtJSのチャートでも、axesで軸を定義し、seriesコンフィグで線グラフや棒グラフを指定します。

実装

チャートのデータはやはりストアで管理します。

なので、まずはモデルとストアを用意しました。

モデル、ストア

/**
 * 貯水量モデルクラス。
 *
 * @class Sample.model.WaterStorage
 * @extend Ext.data.Model
 */
Ext.define('Sample.model.WaterStorage', {
    extend: 'Ext.data.Model',

    fields: [
        {
            // 年月
            name: 'ym',
            type: 'string'
        },
        {
            // 量
            name: 'amount',
            type: 'int'
        }
    ]
});

/**
 * 貯水量ストアクラス。
 *
 * @class Sample.store.WaterStorage
 * @extend Ext.data.Store
 */
Ext.define('Sample.store.WaterStorage', {
    extend: 'Ext.data.Store',

    requires: [
        'Sample.model.WaterStorage'
    ],

    model: 'Sample.model.WaterStorage',

    proxy: 'memory',

    data: [
        { ym: '2016/01', amount: 100 },
        { ym: '2016/02', amount: 50 },
        { ym: '2016/03', amount: 30 },
        { ym: '2016/04', amount: 70 },
        { ym: '2016/05', amount: 50 },
        { ym: '2016/06', amount: 40 },
        { ym: '2016/07', amount: 30 },
        { ym: '2016/08', amount: 20 },
        { ym: '2016/09', amount: 40 },
        { ym: '2016/10', amount: 50 },
        { ym: '2016/11', amount: 60 },
        { ym: '2016/12', amount: 50 }
    ]
});

チャートクラスを作成

Ext.chart.CartesianChartを継承したチャートクラスを作成します。

最低限、store, axes, seriesコンフィグの指定が必要です。

/**
 * チャートクラス。
 *
 * @class Sample.views.main.chart.Panel
 * @extend Ext.chart.CartesianChart
 */
Ext.define('Sample.views.main.chart.Panel', {
    extend: 'Ext.chart.CartesianChart',
    xtype: 'chart_panel',

    requires: [
        'Ext.chart.axis.Numeric',
        'Ext.chart.axis.Category',
        'Ext.chart.interactions.ItemHighlight',
        'Ext.chart.series.Line'
    ],

    store: 'WaterStorage',

    axes: [
        {
            type: 'numeric',
            position: 'left',
            minimum: 0,
            title: {
                text: '貯水量'
            }
        },
        {
            type: 'category',
            position: 'bottom',
            title: {
                text: '年月'
            }
        }
    ],

    series: [
        {
            type: 'line',
            xField: 'ym',
            yField: 'amount',
            style: {
                stroke: '#ad5987',
                lineWidth: 2
            },
            marker: {
                type: 'circle',
                radius: 4,
                lineWidth: 2,
                fill: '#ad5987'
            },
            label: {
                field: 'amount',
                display: 'over'
            }
        }
    ]
});

axesコンフィグには、x軸とy軸の情報を指定します。y軸は貯水量を表現したいので、数値を扱えるtype: 'numeric'、x軸は各年月を表現したいので、どの型でもそのまま分類として扱えるtype: 'category'です。あと、どの位置かをpositionで設定します。

seriesコンフィグには、type: 'line'を設定していますが、これは線グラフのデータ系列です。x軸、y軸のデータとしてモデルのどのフィールドを使うかxField, yFieldで指定します。他のstyleやmarkerなどでスタイルなどの補足的な指定ができます。あと、チャート内に表示されている数値は、labelコンフィグが指定されているためです。

f:id:sham-memo:20170422161755p:plain

これでチャートを使うための基本は押さえました。

次回はExtJSで提供されている色々なチャートを使ってみます。