読者です 読者をやめる 読者になる 読者になる

初心者のためのExtJS入門

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

アプリケーションで使用する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メソッドなどに上記コードを追加してしまうなどの工夫が必要でしょう。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で提供されている色々なチャートを使ってみます。

レイアウト[modern]

今回はmodernのレイアウトを取り上げます。

classicと説明が同じになっているところは、たぶん気のせいです。

Ext.layout.Default

これは特にレイアウトを指定していない場合に適用されているデフォルトのレイアウトです。

この場合、単純にDOMを順番に配置するだけです。アイテムコンポーネントのスタイル次第です。

/**
 * レイアウトDefaultのパネル。
 * 
 * @class Sample.view.main.layout.DefaultPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.DefaultPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_default_panel',

    layout: 'default',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

Ext.layout.Fit

アイテムコンポーネントを、layoutコンフィグを設定した親コンポーネントのサイズまで目一杯広げて設置するレイアウトです。

アイテムコンポーネントが複数ある場合は、表示できるものが1つだけ表示されます。

/**
 * レイアウトFitのパネル。
 *
 * @class Sample.view.main.layout.FitPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.FitPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_fit_panel',

    layout: 'fit',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

Ext.layout.Card

アイテムコンポーネントを、layoutコンフィグを設定した親コンポーネントのサイズまで目一杯広げて設置するレイアウトです。

アイテムコンポーネントが複数ある場合は、インデックス番号がより小さい表示可能な(非表示になっていない)アイテムコンポーネントが1つだけ表示されます。

Ext.layout.Fitに似ていますが、さらに表示するアイテムコンポーネントを切り替える機能を持ちます。

/**
 * レイアウトCardのパネル。
 *
 * @class Sample.view.main.layout.CardPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.CardPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_card_panel',

    layout: 'card',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

    cls: 'main-panel',

    requires: [
        'Sample.view.main.layout.CardPanel'
    ],

    layout: 'fit',

    tools: [
        {
            xtype: 'button',
            text: 'Next',
            ui: 'action',
            handler: function (btn) {
                var mainPanel = btn.up('main_panel'),
                    cardPanel = mainPanel.down('layout_card_panel'),
                    nextActive = cardPanel.items.indexOf(cardPanel.getActiveItem()) + 1;

                if (nextActive >= cardPanel.items.length) {
                    nextActive = 0;
                }

                cardPanel.setActiveItem(nextActive);
            }
        }
    ],

    items: {
        xtype: 'layout_card_panel'
    }
});

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

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

Ext.layout.Float

アイテムコンポーネントをfloatで並べます。

/**
 * レイアウトFloatのパネル。
 *
 * @class Sample.view.main.layout.FloatPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.FloatPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_float_panel',

    layout: 'float',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

Ext.layout.HBox

アイテムコンポーネントを横に並べるレイアウトです。

アイテムコンポーネントが右端に到達しても折り返しされません。

/**
 * レイアウトHBoxのパネル。
 *
 * @class Sample.view.main.layout.HboxPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.HboxPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_hbox_panel',

    scrollable: true,

    layout: 'hbox',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

レイアウトのalignコンフィグで、アイテムコンポーネントの縦方向に対する設定が可能です。alignは↓のように指定します。

layout: {
    type: 'hbox',
    align: 'middle'
}
middle end stretch
縦の位置を中央揃えにします。 縦の位置を下端に揃えます 高さを目一杯広げます
f:id:sham-memo:20170416214143p:plain f:id:sham-memo:20170416214247p:plain f:id:sham-memo:20170416213916p:plain

レイアウトのpackコンフィグで、アイテムコンポーネントの横方向に対する設定が可能です。

center end
横の位置を中央揃えにします 横の位置を右端に寄せます
f:id:sham-memo:20170416214755p:plain f:id:sham-memo:20170416214844p:plain

アイテムコンポーネントflexを指定することで、flexを指定したアイテムコンポーネントだけ目一杯幅を広げることができます。

/**
 * レイアウトHBoxのパネル。
 *
 * @class Sample.view.main.layout.HboxPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.HboxPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_hbox_panel',

    layout: 'hbox',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1',
            flex: 1
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2',
            flex: 2
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3',
            flex: 1
        }
    ]
});

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

flexを指定したアイテムコンポーネントがある場合、余った部分をflexの値の比率で分け合います。

Ext.layout.VBox

アイテムコンポーネントを縦に並べるレイアウトです。

向きが変わるだけで、Ext.layout.HBoxと同じようなことができます。

/**
 * レイアウトVBoxのパネル。
 *
 * @class Sample.view.main.layout.VboxPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.VboxPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_vbox_panel',

    layout: 'vbox',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1'
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2'
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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

レイアウトのalignコンフィグで、アイテムコンポーネントの横方向に対する設定が可能です。alignは↓のように指定します。

middle end stretch
横の位置を中央揃えにします。 横の位置を右端に揃えます 幅を目一杯広げます
f:id:sham-memo:20170416215544p:plain f:id:sham-memo:20170416215641p:plain f:id:sham-memo:20170416215444p:plain

レイアウトのpackコンフィグで、アイテムコンポーネントの縦方向に対する設定が可能です。

center end
縦の位置を中央揃えにします 縦の位置を下端に寄せます
f:id:sham-memo:20170416215801p:plain f:id:sham-memo:20170416215906p:plain

アイテムコンポーネントflexを指定することで、flexを指定したアイテムコンポーネントだけ目一杯高さを広げることができます。

/**
 * レイアウトVBoxのパネル。
 *
 * @class Sample.view.main.layout.VboxPanel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.main.layout.VboxPanel', {
    extend: 'Ext.Panel',
    xtype: 'layout_vbox_panel',

    layout: 'vbox',

    items: [
        {
            xtype: 'component',
            cls: 'item-cmp1',
            html: 'アイテムコンポーネント1',
            flex: 1
        },
        {
            xtype: 'component',
            cls: 'item-cmp2',
            html: 'アイテムコンポーネント2',
            flex: 2
        },
        {
            xtype: 'component',
            cls: 'item-cmp3',
            html: 'アイテムコンポーネント3'
        }
    ]
});

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