lightweight-charts で垂直線を描画する verticalline

javascript

ブラウザ上で動作するtradingviewのlightweight-chartsは使いやすくて高機能で自前でチャートシステム作りたい人には最高なのですが、意外とサンプルコードが少なく、わかりづらかったりします。

今回は垂直線とラベルを表示するコードを作成してみたいと思います。
ISeriesPrimitiveBaseインターフェースを利用して実装してあります。
サンプルプラグインで機能は満たせるのですが、1本描画しか対応していなので複数描画に対応したバージョンにしたいと思います。以下はモジュールとしてimportするコードになります。

vertical-line.js

const VertLineDefaultOption = {
    time: null,
    text: "sample text",
    info: "sample info",
    textColor: "black",
    bgColor: "white",
    showLabel: true,
    lineColor: "white",
    lineWidth: 2,
}

export class VertLine {
    constructor(chart, series, timeLabelInfoArray) {
        this._chart = chart;
        this._series = series;
        this._paneViews = new Array();
        this._timeAxisViews = new Array();
        timeLabelInfoArray.forEach((timeLabelInfo, i) => {

            const vertLineOptions = { ...TimeLabelInfoDefaultOption , ...timeLabelInfo };
            if (vertLineOptions.time === null) {
                throw Error("timeLabelInfo must specify a time key.");
            }
            this._paneViews.push(new VertLinePaneView(this, vertLineOptions));
            this._timeAxisViews.push(new VertLineTimeAxisView(this, vertLineOptions));
        });
    }

    updateAllViews() {
        this._paneViews.forEach(pw => pw.update());
        this._timeAxisViews.forEach(tw => tw.update());
    }

    timeAxisViews() {
        return this._timeAxisViews;
    }

    paneViews() {
        return this._paneViews;
    }
    getTimeAxisViewOnMouse(param) {
        const data = new Array();
        this._timeAxisViews.forEach((timeAxisView, i) => {
            if (timeAxisView.time() == param.time)
                data.push(timeAxisView);
        });
        return data;
    }
}


function positionsLine(positionMedia, pixelRatio, desiredWidthMedia = 1, widthIsBitmap) {
    // メディア座標をピクセル単位に変換
    const scaledPosition = Math.round(pixelRatio * positionMedia);
    // 線の幅をピクセル単位で計算(widthIsBitmapが真の場合は変換せずにそのまま使用)
    const lineBitmapWidth = widthIsBitmap ? desiredWidthMedia : Math.round(desiredWidthMedia * pixelRatio);
    // 線を中央に配置するためのオフセットを計算
    const offset = Math.floor(lineBitmapWidth * 0.5);
    // 線の開始位置を計算
    const position = scaledPosition - offset;
    // 開始位置と線の幅を含むオブジェクトを返す
    return { position, length: lineBitmapWidth };
}


class VertLinePaneRenderer {
    constructor(x, options) {
        this._x = x;
        this._options = options;
    }

    draw(target) {
        if (this._x === null) return;
        target.useBitmapCoordinateSpace(scope => {
            const ctx = scope.context;//CanvasRenderingContext2D
            const position = positionsLine(this._x, scope.horizontalPixelRatio, this._options.lineWidth);
            ctx.fillStyle = this._options.lineColor;
            //チャート上に縦線を描画するコード
            ctx.fillRect(position.position, 0, position.length, scope.bitmapSize.height);
        });
    }
}

class VertLinePaneView {
    constructor(source, options) {
        this._source = source;
        this._options = options;
        this._x = null;
    }

    update() {
        const timeScale = this._source._chart.timeScale();
        this._x = timeScale.timeToCoordinate(this._options.time);
    }

    renderer() {
        return new VertLinePaneRenderer(this._x, this._options);
    }
}

class VertLineTimeAxisView {
    constructor(source, options) {
        this._source = source;
        this._options = options;
        this._x = null;
    }

    update() {
        const timeScale = this._source._chart.timeScale();
        this._x = timeScale.timeToCoordinate(this._options.time);
    }

    visible() {
        //領域外にはみ出したら消去する
        if (this._x <= 0 || this._x >= this._source._chart.paneSize().width)
            return false;
        else
            return this._options.showLabel;
    }

    tickVisible() {
        return this.visible();
    }

    coordinate() {
        return this._x ?? 0;
    }

    time() {
        return this._options.time;
    }

    text() {
        return this._options.text;
    }

    info() {
        return this._options.info;
    }

    textColor() {
        return this._options.textColor;
    }

    backColor() {
        return this._options.bgColor;
    }


}

メインコード

let chart = LightweightCharts.createChart(document.body, { width: 500, height: 500 });
const data = [
    { time: '2018-12-22', open: 75.16, high: 82.84, low: 36.16, close: 45.72 },
    { time: '2018-12-23', open: 45.12, high: 53.90, low: 45.12, close: 48.09 },
    { time: '2018-12-24', open: 60.71, high: 60.71, low: 53.39, close: 59.29 },
    { time: '2018-12-25', open: 68.26, high: 68.26, low: 59.04, close: 60.50 },
    { time: '2018-12-26', open: 67.71, high: 105.85, low: 66.67, close: 91.04 },
    { time: '2018-12-27', open: 91.04, high: 121.40, low: 82.70, close: 111.40 },
    { time: '2018-12-28', open: 111.51, high: 142.83, low: 103.34, close: 131.25 },
    { time: '2018-12-29', open: 131.33, high: 151.17, low: 77.68, close: 96.43 },
    { time: '2018-12-30', open: 106.33, high: 110.20, low: 90.39, close: 98.10 },
    { time: '2018-12-31', open: 109.87, high: 114.69, low: 85.66, close: 111.26 },
];
const candlestickSeries = chart.addCandlestickSeries();// シリーズを追加
candlestickSeries.setData(data); // データを設定

const lineSeries = chart.addLineSeries();
const timeLabelInfoArray = [
    { time: data[data.length - 2].time, text: "price_data", info: "参考情報", bgColor: "yellow" },
    { time: data[data.length - 6].time, text: "labelText", info: "参考情報", bgColor: "yellow" }
];
const vertLine = new VertLine(chart, lineSeries, timeLabelInfoArray);
lineSeries.attachPrimitive(vertLine);

timeLabelInfoは以下のパラメータを設定できるようにしてあります。infoは情報格納するための変数で特に意味ありません。time keyのみ必須にしてあります。

const TimeLabelInfoDefaultOption = {
    time: null,
    text: "sample text",
    info: "sample info",
    textColor: "black",
    bgColor: "white",
    showLabel: true,
    lineColor: "white",
    lineWidth: 2,
}

出来上がりはこんな感じになります。よく見たら垂直線が白線になっててよく見えません。lineColorを変えてやってください。(直せよ。。。)お疲れ様でした。

コメント

タイトルとURLをコピーしました