HOME > 開発者向けBLOG > Sencha Blog >  Sencha Chart でカスタムチャートを作る

Technology Note 開発者向けBLOG

Sencha Blog

Sencha Chart でカスタムチャートを作る

こんにちは、ゼノフィnakamuraです。

この記事は、US Sencha社ブログ Create Custom Charts using Sencha Charts を翻訳したものです。

概要

Ext JS 5 の発売の際に、Sencha はより新しく強力な Sencha Charts パッケージを発表しました (タッチイベントに対応したビルトインサポートもあります) 。つまり、チャートに対して、タッチジェスチャーを行うことで操作することが可能となりました。Sencha Charts はパフォーマンスを考慮して開発されました。そのため、状況に応じて SVG あるいは HTML5 Canvas (もしくはIE8 の場合 VML) を使ってチャートを描画します。 Ext JS 5 Charts Kitchen Sink を使えば Sencha Charts を動作させたり、より詳しく知る事ができます。

この記事では、新しい Sencha Charts パッケージを使うことで、カスタムチャートの作り方を学べます。新しいファイナンシャルチャートを ウィリアム%R指標 で作る過程をお見せします。

この記事は、Sencha Charts のアーキテクチャについてすでにある程度の理解がある前提で書いています。もしアーキテクチャについて知りたいのであれれば以下の記事を参照してください。

新しいファイナンシャルチャートの制作

In this section, you will see how to create a custom stock chart – このセクションでは、カスタムストックチャート 「ウィリアム%R」を作る手順を紹介します。この指標は、モーメンタム・オシレーターであり、直近の終値のレベルと、ある一定の期間内の最安値や最高値を計算して作られます (この一定の期間は、回顧期間とも言われています) 。

一体何のチャートなのか?

データを元にすると、回顧期間を元に計算するには次のデータが必要です。

  • 期間内の最高値
  • 期間内の最安値

上記の値が計算されたら、以下の数式を使う事で %R の値が計算されます。

%R = (期間内の最高値 - 直近の終値)/(期間内の最高値 - 期間内の最安値) * -100

%R の値が計算されたら、次のサンプルのような、%R の値と時間のチャートを描画しなければなりません。

参照: StockCharts.com

技術的には、ウィリアムズ%R指標を作る上で、この記事の範囲として次のものを要求仕様とします。

  1. 以下のパラーメータを必要とします
    1. oversoldLevel – (売られすぎレベル) 基本的には -80に設定されますが、設定変更可能である必要があります。
    2. overboughtLevel – (買われすぎレベル) 基本的には -20に設定されますが、設定変更可能である必要があります。
    3. lookbackPeriod – lookbackPeriod (回顧期間) 基本的には14日に設定されますが、設定変更可能である必要があります。
  2. -100 から 0 の範囲の数値軸に描画されます。
  3. 数値軸には以下のマークを表示します
    1. -80 – oversoldLevel
    2. -20 – overboughtLevel
    3. -50 – middle
  4. 以下のレベルに水平線を描画します。
    1. -80 – oversoldLevel – 実線
    2. -20 – overboughtLevel – 実線
    3. -50 – middle – 点線
  5. overboughtLevel より上のエリアと、oversoldLevel より下のエリアは塗りつぶす必要があります
  6. 指標は Sencha Charts パッケージアーキテクチャを元に作られます。

簡潔にまとめるために、アクシス、レジェンド、マーカー、ツールチップ、テーマの設定は省きました。

組み立ててみましょう

以下の図は、ウィリアムズ%R を作る際に実装しなければいけない特定のクラスと、Sencha Charts のクラスとの関係を示したものです。

実際に手を動かしてみましょう – 実装手順とコード

ハイレベルな要件とデザインが整いましたので、実装を始めましょう。

CustomSenchaCharts.sprite.WilliamPctR

このクラスは、ウィリアムズ%R の -80、-50、-20 のレベルで線を描くスプライトロジックを実装します。そして、-20以上と、-80以下のエリアを塗りつぶすこともしてくれます。これらのレベルは設定可能で、シリーズ設定の一部として扱われます。

このクラスには、3つの重要なメソッドがあります:

  • drawOverBoughtoverboughtLevel (例えば -20) より上のエリアを描画し塗りつぶしてくれます
  • drawOverSoldoversoldLevel (例えば -80) より上のエリアを描画し塗りつぶしてくれます
  • renderAggregates – -80、-50、-20 のレベルに線を描く基本メソッドです。また、drawOverBoughtdrawOverSold メソッドを呼び、overbought と oversold エリアを塗りつぶし、最終的に指定したスタイルでグラフのラインを描画します。

drawOverBought

このメソッドは overbought エリアを探知し、線を描き、指定したスタイルで塗りつぶします。overbought エリアが始まる部分を探知すると、以下のメソッドを使い、y=-20の線が線分と交わる点を見つけます。

そして、エリアの終わりも探知し、y=-20 という線と交わる座標を計算します。

以下がそのメソッドの完全な実装です。

    drawOverBought: function (surface, ctx, start, end, list, xAxis, obLevel) {
        var attr = this.attr,
            i, x, y, x0, y0, obx = 0, oby = obLevel, obStart = false, obEnd = false;
 
        var lbPeriod = attr.lookBackPeriod - 1;
 
        x0 = list[0];
        y0 = list[1];
 
        var tx = x0, ty = y0;
 
        for (i = 3 * lbPeriod; i < list.length; i += 3) {
            x = list[i];
            y = list[i + 1];
 
            //detect if the ob starts
            if (ty <= y && ty <= oby && y >= oby) {
 
                //find the x co-ordintate of the point of intersection
                obx = x - (((y-oby)*(x-tx))/(y-ty));
 
                //start drawing the path
                ctx.beginPath();
                ctx.moveTo(obx, oby);
 
                obStart = true;
                obEnd = false;
            }
 
            //detect if the ob ends
            if (ty >= y && ty >= oby && y <= oby) {
                obx = tx + (((x-tx)*(ty-oby))/(ty-y));
                ctx.lineTo(obx, oby);
 
                ctx.closePath();
                ctx.fill();
 
                obStart = false;
                obEnd = true;
            } 
 
            //keep drawing the line
            if (y >= oby) {
                //if start was not detected - open start
                if (!obStart) {
                    ctx.beginPath();
                    ctx.moveTo(x0, oby);
                    ctx.lineTo(x0, y0); 
 
                    obStart = true;                   
                }
 
                ctx.lineTo(x, y);
            }
 
            tx = x, ty = y;
        }
 
        //if end is not detected
        if (!obEnd) {
            ctx.lineTo(x, oby);
            ctx.closePath();
            ctx.fill();
        }
    }

drawOverSold

このメソッドは論理的には drawOverBought と同じ働きをして、oversold エリアを指定されたスタイルで塗りつぶします。

drawOverSold: function (surface, ctx, start, end, list, xAxis, osLevel) {
        var attr = this.attr,
            i, x, y, x0, y0, osx = 0, osStart = false, osEnd = false, osy = osLevel;
 
        var lbPeriod = attr.lookBackPeriod - 1;
 
        x0 = list[0];
        y0 = list[1];
 
        var tx = x0, ty = y0;
 
        for (i = 3 * lbPeriod; i < list.length; i += 3) {
            x = list<i>;
            y = list[i + 1];
 
            //detect if the os starts
            if (ty >= y && ty >= osy && y <= osy) {
 
                //find the x co-ordintate of the point of intersection
                osx = tx + (((x-tx)*(ty-osy))/(ty-y));
 
                ctx.beginPath();
                ctx.moveTo(osx, osy);
 
                osStart = true;
                osEnd = false;
            }
 
            //detect if the os ends
            if (ty <= y && ty <= osy && y >= osy) {
                osx = x - (((y-osy)*(x-tx))/(y-ty));
                ctx.lineTo(osx, osy);
 
                ctx.closePath();
                ctx.fill();
                osStart = false;
                osEnd = true;
            } 
 
            //keep drawing the line
            if (y <= osy) {
                //if start was not detected - open start
                if (!osStart) {
                    ctx.beginPath();
                    ctx.moveTo(x, osy);
                    ctx.lineTo(x, y); 
 
                    osStart = true;                   
                }
 
                ctx.lineTo(x, y);
            }
 
            tx = x, ty = y;
        }
 
        //if end is not detected
        if (!osEnd) {
            // console.log('closing!!');
            ctx.lineTo(x, osy);
            ctx.closePath();
            ctx.fill();
        }
    }

renderAggregates

このメソッドはシリーズのレンダリング中に呼ばれ、シリーズの全てのスプライトの描画を担当します。このメソッドは以下のことができるようにオーバーライドしなければなりません。

  • -80、-50、-20 レベルで水平線を描く
  • シリーズの線を描く
  • overbought と oversold エリアを描画して塗りつぶす

ウィリアムズ%R のためにスプライトを描画するには、まず、overboughtLeveloversoldLevel を フレームワークが使用する 座標システムに変換しなければなりません。

        var obLevel = attr.overboughtLevel * yy + dy;
        var osLevel = attr.oversoldLevel * yy + dy;
        var midLevel = -50 * yy + dy;

ctx は基になるレンダリングエンジン (Canvas または SVG) への参照です。

座標が変換されたら、水平線を描きます。実線を -80 と -20に、そして点線を -50に。

        //Draw overbought, oversold and -50 mark lines
        me.drawYLine(ctx, xLen, obLevel);
        me.drawYLine(ctx, xLen, osLevel);
        me.drawYLine(ctx, xLen, midLevel, true);

次に、変換された値を渡して drawOverSolddrawOverbought メソッドを呼び出して、これらのエリアを描いて塗りつぶします。

        //Draw oversold areas
        me.drawOverSold(ctx, list, osLevel);
 
        //Draw overbaught areas
        me.drawOverBought(ctx, list, obLevel);

最後に、 Ext.chart.series.sprite.Line クラスの既存のメソッドである drawStroke を呼びだしてパスを描きます。

        //draw stroke
        me.drawStroke(surface, ctx, start, end, list, rect[1] - pixel);
        ctx.stroke();

drawYLine

指定した y 座標に、点線または実践の水平線を描くプライベートメソッドです。

    drawYLine: function(ctx, length, y, dashed) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(length, y);
        ctx.closePath();
 
        var linedash;
        if (dashed) {
           if (ctx.getLineDash) {
             lineDash = ctx.getLineDash();
             ctx.setLineDash([3]);
           } else {
             lineDash = ctx.lineDash;
             ctx.lineDash = [3];
          }
        }
       ctx.stroke();
       if (dashed) {
           if (ctx.setLineDash) {
               ctx.setLineDash(lineDash);
           } else {
             ctx.lineDash = lineDash;
          }
        }

これでスプライトクラスができたので、次のクラス CustomSenchaCharts.series.WilliamPctR に移りたいと思います。ここではシリーズに関係したロジックを実装します。

CustomSenchaCharts.series.WilliamPctR

The series class accepts the store and following specific parameters: シリーズクラスはストアと以下の特定のパラメータを受け取ります。

  • overboughtLevel
  • oversoldLevel
  • lookBackPeriod

ストアには、安値(low)、高値(high)、直近(close) のフィールドが含まれます。このシリーズクラスは lookBackPeriod (回顧期間、デフォルトは14日間) を元に%R値を計算します。

以下の constructor のコードは、%R を計算し、渡されたストアのレコード内にセットしています:

    constructor: function (config) {
 
        var me = this;
 
        var st = Ext.data.StoreManager.lookup(config.store);
        var recs = st.getRange();
        var highs = Ext.Array.pluck(Ext.Array.pluck(recs, "data"), config.highField);
        var lows = Ext.Array.pluck(Ext.Array.pluck(recs, "data"), config.lowField);
 
        var lpPeriod = config.lookBackPeriod - 1;
 
        st.each(function (item, index, length) {
            if (index < lpPeriod) {
                item["pctr"] = "";
                return;
            }
 
            //get highest high of last 14 days
            var maxHigh = Ext.Array.max(Ext.Array.slice(highs, index - lpPeriod, index + 1));
 
            //get lowest low of last 14 days
            var minHigh = Ext.Array.min(Ext.Array.slice(lows, index - lpPeriod, index + 1));
 
            //calculate %R and set it on the record
            var pctr = ((maxHigh - item.data[config.closeField])/(maxHigh - minHigh)) * -100
            item.data.pctr = pctr;
        });
 
        this.callParent(arguments);
    }

レコードの %R値を更新したので、最後にもう1つ実装しなければいけないメソッドがあります。実際にはオーバーライドですが、WilliamPctR スプライトをレンダリングするタイミングで、スプライトのコンフィグを手に入れるために呼び出されます。別々のスプライトを描いたり塗りつぶしたりできるように overboughtLeveloversoldLevel、そして lookBackPeriod をスプライトに渡すように実装しなければなりません:

    getDefaultSpriteConfig: function () {
        var me = this,
            parentStyleConfig = me.callParent(arguments);
 
        return Ext.apply(parentStyleConfig, {
            overboughtLevel: me.config.overboughtLevel,
            oversoldLevel: me.config.oversoldLevel,
            lookBackPeriod: me.config.lookBackPeriod
        });
    }

最後に1つのクラスが残っていますね。CustomSenchaCharts.chart.WilliamPctR、ウィリアムズ%R指標をアプリ内で作らせてくれるチャートです。

CustomSenchaCharts.chart.WilliamPctR

このチャートクラスは CartesianChart を継承し、呼び出し元が numeric アクシスを指定することになっています。そして、指定された numeric アクシスを使って %R値を描きます。クラスはクラスの初期化時に呼び出される initConfig の中で、以下の ウィリアムズ%R に関係した追加のアクシスのコンフィグをセットしています。

  • fields – 数値軸上でレンダーされるフィールド。’pctr’ にセットされます。
  • minimum – axis で表示される最小値。-100にセットされます。
  • maximum – axis で表示される最高値。0 にセットされます。なぜなら %R は 0 と -100の間を変動するから。
Ext.define("CustomSenchaCharts.chart.WilliamPctR", {
    extend: 'Ext.chart.CartesianChart',
    requires: ['CustomSenchaCharts.series.WilliamPctR', 'CustomSenchaCharts.sprite.WilliamPctR'],
    xtype: 'williampctrchart',
 
    initConfig: function(config) {
 
        var series = config.series[0];
        var obLevel = series.overboughtLevel;
        var osLevel = series.oversoldLevel;
 
        Ext.Array.each(config.axes, function(axis, index, recs) {
            if (axis.type === 'numeric') {
                Ext.apply(axis, {
                    fields: ['pctr'],
                    maximum: 0,
                    minimum: -100,
                    renderer: function (value, layoutContext, lastValue) {
                        if (value == osLevel || value == -50 || value == obLevel){
                            return value;
                        } else {
                            return "";
                        }
                    }
                });
            }
        });
 
        this.callParent(arguments);
    }
});

あともう少しで終わりですが、最後に上のチャートをアプリケーション内で実行してみなければいけません。

動作確認

前のセクションでは、WilliamPctR チャートの xtype を ‘williampctrchart‘ と定義しました。サンプルアプリケーション内でどのように動くか見てみましょう。

上のチャートをアプリケーション内で使用するには、Ext.create を呼び出してインスタンス化するか、xtype を使用するかです。例えば、Ext JS コンテナーのアイテムの一つとして追加するコードは次の様になります。

{
                xclass: 'CustomSenchaCharts.chart.WilliamPctR',
                height: 250,
                docked: 'bottom',
                insetPadding: 0,
                background: 'white',
                series: [
                    {
                        store: 'Apple',
                        type: 'williampctr',
                        xField: 'date',
                        yField: 'pctr',
                        highField: "high",
                        lowField: "low",
                        closeField: "close",
                        overboughtLevel: -20,
                        oversoldLevel: -80,
                        lookBackPeriod: 14,  //in days
                        style: {
                            stroke: 'rgba(237,123,43,0.75)',
                            fill: 'rgba(237,123,43,0.1)',
                            miterLimit: 1
                        }
                    }
                ],
                axes: [
                    {
                        type: 'numeric',
                        position: 'left',
                        style: {
                            axisLine: false
                        }
                    },
                    {
                        type: 'time',
                        position: 'bottom',
                        fields: ['date'],
                        style: {
                            strokeStyle: '#666',
                            estStepSize: 150
                        },
                        dateFormat: 'Y',
                        segmenter: {
                            type: 'time',
                            step: {
                                unit: 'y',
                                step: 1
                            }
                        },
                        label: {
                            fontSize: 10,
                            fillStyle: '#666'
                        }
                    }
                ]
            }

次の図が ウィリアムズ%R の出力です。

完全なコードは GitHub からダウンロードすることができます。注:このコードは Ext JS (5.0.1)を使用して書かれました。

まとめ

この記事では、Cartesian 座標システムを元に、カスタムチャート、シリーズ、そしてスプライトを実装することで、カスタムストックチャートを使る方法を解説しました。そして、カスタムストックチャートが Ext JS 5 アプリ内で使うにはどうしたらいいかも見てきました。この記事が Sencha Charts パッケージ、重要なクラス、それらの役割、そして相互作用の理解を深め、カスタムチャートの作り方を伝えられている事を願っています。

参考文献

PAGETOP