株価チャート chartjs-chart-financial を試行錯誤で使ってみた　3

## 移動平均線の追加と表示期間の変更

### 移動平均等指標の追加
移動平均線、ボリンジャーバンド、一目均衡表などローソク足とスケールが同じ指標については、データセット（`chart.config.data.datasets`）に出来高と同じように追加していきます。
```Javascript
chartData.trend{
    name: 'MA',
    data: [
        //MA時系列はvolumesと同じ構造　[{x:タイムスタンプ, y:値}, ...]
        {label: 'MA25', series: MA時系列1, },
        {label: 'MA75', series: MA時系列2, },
    ]
}
const colors = { MA: ['fuchsia', 'springgreen', 'darkorange', 'mediumblue'] ,};
```
このようなデータを作成した場合、ラインデータは以下のようにすると追加できます。
```Javascript
for(let i =0;i<chartData.trend.data.length;i++){
    const ds = {
        label: chartData.trend.data[i].label,
        type: 'line',
        data: chartData.trend.data[i].series,
        borderColor: colors.MA[i],  
        backgroundColor: colors.MA[i],
        borderWidth: 1,
        pointRadius: 0, // 0はポイントマークなし   
        yAxisID: "yAxisMain",
    }    
    chart.config.data.datasets.push(ds);
}
```
軸スケールが異なる出来高を追加する場合には、ローソク足と同軸で表示するために`yAxisID`にローソク足のidここでは「yAxisMain」を指定します。
`pointRadius`を指定しないと個々のデータが円形ポイントで表示されるので、0を指定して線のみを表示します。

なお、Chart.jsで色の設定は16進数、RGB、HSLのいずれかで指定すべきのようですが[^1]、WebColorのColorNameでも表示されるようです。

パラボリックのようなマークを表示する場合には、　`type: 'scatter',`に加えて`pointRadius: 1,`のように設定します。上昇トレンドと下降トレンドを区別してマークごとに色を変えたい場合には、データ数と同じ数（日数）の色配列を`borderColor`または`backgroundColor`に代入します[^2]。
ドキュメントによると、backgroundColorとborderColorはScriptableでfunctionを指定できるようです[^3]が、これは試していません。

一目均衡表の雲のようにライン間を塗りつぶしたい場合には、fillプロパティを利用します[^4]。
```Javascript
if(data.trend.name == 'Ichimoku'){
    if (i == 3)
        fill = {
            // index 3が先行スパン1 次の4が先行スパン2とすると
            target: '+1',
            above: 'gray',
            below: 'yellow',
        }
}
 const ds = {
    label: data.trend.data[i].label,
    type: data.trend.name == 'line',
    data: data.trend.data[i].series,
    borderColor: colorsTrend[i],  
    backgroundColor: colorsTrend[i],
    borderWidth: 1,
    fill: fill,
    pointRadius: 0,   
}    
```
`target`の+記号は対象データセットを相対的に指定する場合で、この場合は次の先行スパン2に対して高い場合と低い場合に色を変えて塗りつぶすようになります。

#### トレンド等計算関数
`chart-trend.js`

1. getWeekSeries(dailydata)
    日足時系列データから週足データを取得します
    戻り値：[{x:タイムスタンプ, d:日付(yyyy-MM-dd), o:始値, h;高値 ,l:安値 ,c:終値 , (v:出来高 )} ... ]
    
2. getMonthSeries(dailydata)
    日足時系列データから月足データを取得します
    戻り値：[{x:タイムスタンプ, d:日付(yyyy-MM-dd), o:始値, h;高値 ,l:安値 ,c:終値 , (v:出来高 )} ... ]
    
3. calcMa(series_source, interval, set_null = true)
    時系列データから移動平均時系列データを計算します
    戻り値：[{x:タイムスタンプ, y:値またはnull} ... ]
    
4. function calcKairi(data)
    chartDataのMAパラメータから乖離を計算します
    戻り値：[{label: 'MA5', params: 5, series:[{x:タイムスタンプ, y:%値またはnull} ... ]}, ... ]
    
5. calcStage(series_source, params)
    小次郎講師の大循環分析のステージを計算 params移動平均3本の期間
    戻り値：[{x:タイムスタンプ, y:値(1-6)またはnull} ... ]
    
6. function calcIchi(series_source)
    一目均衡表の計算
    先行スパンを計算するために、それぞれ25日先（休日は考慮せず）までデータ（nullを含めて）を保持しています
    戻り値：[{label:'転換線', series:[{x:タイムスタンプ, y:値またはnull} ...]}, {label:'基準線', series:[{x:タイムスタンプ, y:値またはnull} ...]}, ... ]
    
7. function calcMacd(series_source, params)
    MACDを計算します
    paramsは[短期 ,長期 ,シグナル移動平均期間 ]
    戻り値：[{label:'MACD', params:[12, 26], series:[{x:タイムスタンプ, y:値またはnull} ...]}, {label: 'Signal', param:9, series:[{x:タイムスタンプ, y:値またはnull} ...]}]
    
8. function calcRsi(series_source, params, simple = false)
    RSIを計算します
    paramsは[RSI期間, 移動平均期間]
    戻り値：[{label:'Wilderまたはsimple', param:14, series:[{x:タイムスタンプ, y:値またはnull} ...] }, {label: 'Signal', param:9, series:[{x:タイムスタンプ, y:値またはnull} ...]}]
    
9. function calcRci(series_source, param)
    RCIを計算します
    paramsは期間
    戻り値：{label:'RCI9', param:9, series:[{x:タイムスタンプ, y:値またはnull} ...]}
    
10. calcSar(series_source, params)
    パラボリックSARを計算します
    paramsは[start, step, max]
    戻り値：[{label:'Parabolic', params:[0.02, 0.02, 0.2], series:[{x:タイムスタンプ, y:値またはnull, trend:1または-1} ...]}]
    ＊トレンドは1が上昇 -1が下降
    
11. calcBollinger(series_source, params)
    ボリンジャーバンド（1σ,2σ,3σ）を計算します
    paramsは[移動平均]
    戻り値；[{label:'MA20', params:20, series:[{x:タイムスタンプ, y:値またはnull} ...]}, {label:'+1σ', params:20, series:[{x:タイムスタンプ, y:値またはnull} ...]}, {label:'-1σ', params:20, series:[{x:タイムスタンプ, y:値またはnull} ...]}, ... ]

### チャート表示期間の変更

データ量（日数）が多い場合にはローソク足が潰れてしまうため、表示期間を変更して再表示する機能が必要になります。
表示期間を変更する方法として、以下の方法を考えました。
1. chart.config.options.scales.x.minと.maxを変更する方法
2. あらかじめ絞り込んだデータをデータセットに格納する方法

その他にも横軸をスクロールさせる方法もあるようです。

チャートのオプションでx軸のmaxとminを変更する方法がchart.jsとしては王道だと思うのですが、y軸は表示期間で自動に調整されないためスクリプトでy軸も最大値と最小値を計算してmaxとminを指定する必要があることと、表示期間が少ないのにもかかわらずローソク足が潰れて表示される場合が度々見られました。

個人的には、期間を変更するたびに各データセットの元データをフィルタして新たに設定し直す(2)の方法がy軸のスケールも自動で調整されるので簡単だと感じています。

その他にも、x軸をスクロールしてチャートを見やすくする方法もあるようですが、これについてはまだ十分には理解していません。

今回は、表示開始日と終了日を設定してチャートを描画するように設計してみました。
中央のレンジバーは表示期間の終了日と連動しており、終了日に対して表示テキストボックスに設定した日数をさかのぼって開始日に設定することで、開始日から終了日の間のローソク足が表示されます。開始日と終了日のカレンダーまたは表示データ数を変更することでも表示が変化するはずです。
レンジバーのステップ期間を5としているため、レンジバーだけでは最小または最大日が表示できないこともあるかもしれません。

フィルタしたデータを直接データセットに代入する(2)の方法がデフォルトですが、「x軸変更」チェックボックスをチェックした状態にすることでデータセットには全期間のデータを代入したのち x 軸スケールの max と min を変更することで表示期間を変更する方法を確認することができます。

なおx軸のmaxとminを変更したときにスケールを変更しない場合との比較ができるように、y軸調整切り替えボタンで表示期間中の最高値と最安値を基準にスケールを切り替えられるように工夫してみました。

いずれの方法においても、移動平均は表示期間変更のたびには計算せずに銘柄変更時、表示方法の変更時、パラメータ変更時に全データで計算して変数に保持するようにしています。そのため、データ日数が多い場合には無駄が多く切り替え時に時間がかかりすぎるかもしれません。

イベントリスナーを利用しましたが、数が多くて複雑になってしましました。
レイアウトやイベントに関しても十分には理解していないため不具合もあると思います。


### その他の機能

「データをテーブル表示」のチェックボックスをクリックすることで、トレンドを含めたデータをテーブル表示することで計算値のチェックをできるようにしています。

```HTML:SelectCsv.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.0.0"></script>
<script src="https://www.chartjs.org/chartjs-chart-financial/chartjs-chart-financial.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
<style>
    table{ font-size: small; border-width: thin; border-style: solid; border-collapse: collapse; }
    th{border-width: thin; border-style: solid;}
    td{text-align: right; border-width: thin; border-style: solid;}
    form{width: 540px; height: 90px;}
    .settingArea{border: gray solid 1px; font-size: small; padding: 0px; height: 70px; width: auto;}
    .rangeBar{width: 500px;}
    .info{font-size: small;}
    .json{width: 460px; height: 320px;}
    .tbSmall{width: 30px; height: auto; text-align: right;}
    .tbMedium{width: 82px; text-align: left;}

    .canvas { width: 1000px; height: 340px; position: relative; }
    .main_chart { display: flex; width: 100%; flex-direction: column; justify-content: center; align-items: center; height: 300px; }
</style>
</head>
<body>

<div><input type="file" id="selectfile"><label class="info" id="info"></label></div>
<div>
    <form name="settingForm" id="form">
        <div class="settingArea">
            <div>
                <label><input type="date" id="dateFrom" value="">～</label>
                <label><input type="date" id="dateTo" value=""></label>
                <label >表示<input type="text" class="textbox tbSmall" id="tbBarCount" value="150"></label>
                <label style="display: none;"><input class="reload" type="radio" name="line" value="3" >line</label>
                <label><input type="checkbox" name="volume" checked>出来高</label>
                <label style="margin-left:10px;"><input class="reload" type="checkbox" name="maxmin" id="checkByMini">x軸変更</label>
                <label style="display: none;" id="adjustY"><input class="reload" type="checkbox" name="adjust" id="checkAdust" checked>y軸調整</label>
        
                <label style="display: none;" style="margin-left:20px;"><input class="reload" class="checkbox" type="checkbox" name="subchart" id="checkSub" checked >2段表示</label>
            </div>
            <div>
                <input class="rangeBar" id="rangeBar" type="range" step="5" value="100">
            </div>
            <div>
                <label><input class="reload" type="radio" name="trend" value="MA" checked>MA
                <input type="text" class="textbox tbMedium" name="maInterval" value="5, 25, 75, 200"></label>
                <label><input reload="reload" type="radio" name="trend" value="Bollinger">Bollinger</label>
                <label><input class="reload" type="radio" name="trend" value="Ichi">一目均衡表</label>
                <label><input class="reload" type="radio" name="trend" value="Parabolic">Parabolic</label>
                <label><input class="reload" type="radio" name="trend" value="None">None</label>
            </div>
        </div>
    </form>
</div>        

<div class="container">
    <div class="canvas" style="width:100%">

        <canvas id="chart" class="main_chart"></canvas>
        <canvas id="sub" class="sub_chart" style="display: none;"></canvas>

        <label><input type="checkbox" id="checkJson" onchange="checkJson(this)"> Jsonデータ表示</label>
        <label><input type="checkbox" id="checkTable" onchange="writeTable(this)"> データをテーブル表示</label>
        <table id='price'></table>
    </div>
    <textarea class="json" style="display: none;" id="json"></textarea>
</div> 


<script src="chart-formEvent.js"></script>

<script src="chart-viewer2.js"></script>
<script src="chart-csv-reader.js"></script>
<script src="chart-trends.js"></script>
<script src="chart-writeTable.js"></script>

</body></html>
```

```Javascript:chart-formEvent.js
// ------------- Formのラジオボタン、スライダー等のchangeイベント
const form1 = document.getElementById("form");
form1.addEventListener("change", (event) => {
    if(event.target.id === 'checkScale'){
        document.write(event.target.id);
        return;
    }
    // まずはレンジバー、カレンダー、データ数をチェックして　開始日と終了日を更新
    // レンジバーのstepは5にしたため、min + barCountを5のあまりとして最大も表示できるようにする
    // barのmaxは変わらない　bar.valueが変わるたびにtbBarCountの間隔を保持してdateFromとdateToが変化する
    // minの場合はカレンダーを直接変更して表示する
    if (event.target.id === "dateTo") {
        const indexTo = chartData.candle.findIndex(d => d.d >= document.getElementById("dateTo").value);
        document.getElementById("rangeBar").value = indexTo;        
        document.getElementById("dateFrom").value = chartData.candle[indexTo - Number(document.getElementById("tbBarCount").value)].d;
    } else if (event.target.id === "dateFrom"){
        let indexFrom = chartData.candle.findIndex(d => d.d >= document.getElementById("dateFrom").value);
        if (indexFrom > chartData.candle.length - document.getElementById("tbBarCount").value){
            indexFrom = chartData.candle.length - document.getElementById("tbBarCount").value;
            document.getElementById("dateFrom").value = chartData.candle[indexFrom].d;
        }
        document.getElementById("rangeBar").value = indexFrom + Number(document.getElementById("tbBarCount").value);
        document.getElementById("dateTo").value = chartData.candle[document.getElementById("rangeBar").value].d;
    } else if (event.target.id === "rangeBar") {
        // レンジバーの変更時
        document.getElementById("dateTo").value = chartData.candle[document.getElementById("rangeBar").value].d;
        const index = document.getElementById("rangeBar").value - Number(document.getElementById("tbBarCount").value);
        document.getElementById("dateFrom").value = chartData.candle[index < 0 ? 0 : index].d;
    } else if (event.target.id === "tbBarCount") {
        const max = chartData.candle.length - 1;
        const min = (max - Number(document.getElementById('tbBarCount').value) * 1) % 5 + Number(document.getElementById('tbBarCount').value);
        document.getElementById("rangeBar").min = min;
        const index = document.getElementById("rangeBar").value - Number(document.getElementById("tbBarCount").value);
        document.getElementById("dateFrom").value = chartData.candle[index < 0 ? 0 : index].d;
    }
    // ここでセッティングの読み込み
    let setting = readSettingForm();

    const name = event.target.name;
    switch(name){
        case 'maxmin':
            if (event.target.checked) {
                document.getElementById("adjustY").style.display = "inline-block";
            } else {
                document.getElementById("adjustY").style.display = "none";
            }
        case 'adjust':
             setting = reload(setting);
            break;
        case 'maInterval':
            setting = reload(setting);
            setting.reload = 'trend';
            break;
        case 'trend':
            if(event.target.value === 'MA')
                document.getElementsByName('maInterval')[0].style.display = "inline-block";
            else
            document.getElementsByName('maInterval')[0].style.display = "none";
        case 'oscillator':
            setting.reload = name;
            break;
        case 'ashi':
            changeAshi(setting.ashi);
            setting.reload = 'trend oscillator';
    }

    if (document.getElementById("checkByMini").checked && ! setting.reload) {
        // chartのx.max x.min で期間を設定する場合
        chart.config.options.scales.x.min = setting.range.from.x;
        chart.config.options.scales.x.max = setting.range.to.x;
        if(document.getElementById('checkAdust').checked){
            const series = chartData.candle.filter(d => d.d >= setting.range.from.d && d.d <= setting.range.to.d);
            // y軸調整上2桁で最大最小設定
            let maxY = series.map(d => d.h).reduce(function (a, b) {return Math.max(a, b);});
            let minY = series.map(d => d.l).reduce(function (a, b) {return Math.min(a, b);});
            minY = Math.floor(Math.floor(minY) / Math.pow(10, String(Math.floor(minY)).length - 2))*Math.pow(10, String(Math.floor(minY)).length - 2);
            maxY = Math.ceil(Math.ceil(maxY) / Math.pow(10, String(Math.ceil(maxY)).length - 2))*Math.pow(10, String(Math.ceil(maxY)).length - 2);
            chart.config.options.scales.yAxisMain.min = minY;
            chart.config.options.scales.yAxisMain.max = maxY;
        }
        chart.update();
    } else {
        // データセットにフィルタしたデータを代入する場合
        changeDatasource(setting);
    }
});

// --------------- settingFormのラジオボタン等を確認してsetting変数に格納
function readSettingForm(){
    const ashiNames = ['daily', 'weekly', 'monthly'];
    let setting = {
        ashi:{},
        range:{ from: {}, to: {}, },
        trend:{},
        oscillator:{},
        isVolume: document.settingForm.volume.checked,
        reload: '',
    };
    for (let i = 0;i < document.settingForm.trend.length;i++){
        if(document.settingForm.trend[i].checked){
            setting.trend.name = document.settingForm.trend[i].value;
            break;
        }
    }
    if(setting.trend.name == 'MA'){
        let strParamMa = document.getElementsByName('maInterval')[0].value.split(/,|\s/);
        setting.trend.params = strParamMa.map( d => Number(d)).filter(d => d > 0);
    } 
    const dateFrom = document.getElementById("dateFrom").value;
    const dateTo = document.getElementById("dateTo").value;
    const series = chartData.candle.filter(d => d.d >= dateFrom && d.d <= dateTo);
    setting.range = { from: {x:series[0].x, d:series[0].d}, to: { x:series[series.length - 1].x, d:series[series.length - 1].d } };
    return setting;
}

function reload(setting){
    // トレンド等再計算するために期間をすべてに戻して再読み込み
    const rangeOrg = setting.range;
    setting.range = {
        from: { x:chartData.candle[0].x, d:  chartData.candle[0].d },
        to: { x:chartData.candle[chartData.candle.length - 1].x, d: chartData.candle[chartData.candle.length - 1]. d},
    };
    changeDatasource(setting);
    setting.range = rangeOrg;
    return setting;
}
// --------------- textarea 表示切替
function checkJson(checkbox) {
  if (checkbox.checked) {
    document.getElementById("json").style.display = "block";
  } else {
    document.getElementById("json").style.display = "none";
  }
}
```

```Javascript:chart-viewer2.js
// <-------------- チャート用の変数
const colors = {
    candle:{ up: '#ff4500', down: '#32cd32', unchanged: '#696969',},
    volume: '#b0c4de',
    MA: ['#ff00ff', '#00ff7f', '#ff8c00', '#0000cd'] ,
    Parabolic: ['#ff00ff', '#00bfff'],
    Bollinger: ['#00ff7f', '#ffa500' , '#ffa500', '#ff0000', '#ff0000' , '#9400d3', '#9400d3'],
    Ichi: ['#ff1493', '#0000ff' , '#ffd700', '#e6e6fa', '#fa8072'],
    cloud: ['#e0ffff', '#f5f5f5' ],
};

const ctx = document.getElementById("chart").getContext("2d");
const chart = new Chart(ctx, {
    type: "candlestick",
    data: { datasets: [] },
});


// チャートに表示するデータを格納したグローバルデータ変数
let chartData = {};

// 銘柄（ファイル）変更
function changeBrand(dailyPrices){
    chartData = {
        candle: dailyPrices,
        volume: dailyPrices.find(d => d.v) ? dailyPrices.map(d => {return {x:d.x, y:d.v}}): undefined,
    };
    document.getElementById('info').textContent = `${dailyPrices[0].d} ${dailyPrices[dailyPrices.length - 1].d}  ${dailyPrices.length.toLocaleString()}本`;
    setCalendar();
    const setting = readSettingForm();
    setting.reload = 'trend oscillator';
    changeDatasource(setting);
    // Chartでフィルタチェック時にはデータセット後にmin maxを設定
    if(document.getElementById('checkByMini').checked)
        changePeriod(false);
}

function setCalendar(){
    const max = chartData.candle.length - 1;
    // step=5 でmaxは表示させたい minはバー移動では表示できないことも
    const min = (max - Number(document.getElementById('tbBarCount').value) * 1) % 5 + Number(document.getElementById('tbBarCount').value);
    document.getElementById('rangeBar').max = max;
    document.getElementById("rangeBar").min = min;
    // 銘柄変更時は最終日にバー位置設定
    document.getElementById('rangeBar').value = max;
    // バーの位置が表示最終日とする
    document.getElementById('dateTo').value = chartData.candle[document.getElementById('rangeBar').value].d;
    document.getElementById('dateFrom').value = chartData.candle[document.getElementById('rangeBar').value - document.getElementById('tbBarCount').value].d;

}

function changeDatasource(setting){
    if(setting.reload.includes('trend') && setting.trend.name !== undefined){
        switch (setting.trend.name){
            case 'MA':
                chartData.trend = {
                    name: setting.trend.name,
                    data: [],
                }
                for(let i = 0; i < setting.trend.params.length ; i++){
                    const ma = calcMa(chartData.candle, setting.trend.params[i], true);
                    chartData.trend.data.push({label: setting.trend.name + setting.trend.params[i], params: setting.trend.params[i], series: ma});
                }
                break;
            case 'Bollinger':
                chartData.trend.data = calcBollinger(chartData.candle, [20]);
                break;
            case 'Ichi':
                chartData.trend.data = calcIchi(chartData.candle);
                break;
            case 'Parabolic':
                chartData.trend.data = [calcSar(chartData.candle, [0.02, 0.02, 0.2])];
                break;
            case 'None':
                chartData.trend.data = [];
        }
    }
    const datasets = [];
    // 四本値
    // 一目はdateToに対して14日先のデータも表示する（一目のseriesは先行スパン分の将来の日付データも含まれている）
    const rangeX = 
        setting.trend.name === 'Ichi' ?
            [searchOffsetX(chartData, setting.range.from.x), searchOffsetX(chartData, setting.range.to.x)]
            :
            [setting.range.from.x, setting.range.to.x];
    const data_candles = {
        data: chartData.candle.filter(d => d.x >= rangeX[0] && d.x <= rangeX[1]),
        color: colors.candle,
        yAxisID: "yAxisMain",
    }
    datasets.push(data_candles);
    if(setting.isVolume && chartData.volume !== undefined){
        // 出来高データを追加
        const ds_volume = {
            type: 'bar',
            label: 'volume',
            data: chartData.volume.filter(d => d.x >= rangeX[0] && d.x <= rangeX[1]),
            borderColor: colors.volume,
            backgroundColor: colors.volume,
            borderWidth: 1,
            pointRadius: 0,
            yAxisID: "yAxisSub",
        }
        datasets.push(ds_volume);
    }
    if(chartData.trend !== undefined){
        // トレンド追加
        for(let i =0;i<chartData.trend.data.length;i++){
            ds = {
                label: chartData.trend.data[i].label,
                data: chartData.trend.data[i].series.filter(d => d.x >= rangeX[0] && d.x <= rangeX[1]),
                borderWidth: 1,
                yAxisID: "yAxisMain",
            }
            switch(setting.trend.name){
                case 'Parabolic':
                    const p_colors = [];
                    for(let i = 0; i < ds.data.length; i++){
                        p_colors[i] = ds.data[i].trend > 0 ? colors['Parabolic'][0] : colors['Parabolic'][1];
                    }
                    ds.type = 'scatter';
                    ds.borderColor = p_colors;
                    ds.backgroundColor = p_colors;
                    ds.pointRadius = 1;
                    break;
                case 'Ichi':
                    if (i == 3){
                        // 一目先行スパン1（i=3）で雲を塗りつぶし brek; なしでdefaultも実行
                        ds.fill = {
                            target: '+1',
                            above: colors['cloud'][0],
                            below: colors['cloud'][1],
                        }
                    }
                default:
                    ds.type = 'line';
                    ds.borderColor = colors[setting.trend.name];
                    ds.backgroundColor = colors[setting.trend.name];
                    ds.pointRadius = 0; // 0はポイントマークなし
                    break;
            }
            datasets.push(ds);
        }
    }
    chartData.datasets = datasets;
    refreshChart(chartData);
}

function searchOffsetX(chartData, x){
    const currentIndex = chartData.trend.data[0].series.findIndex(d => d.x >= x);
    const offset = currentIndex + 14 >= chartData.trend.data[0].series.length ? chartData.trend.data[0].series.length - 1 : currentIndex + 14;
    return chartData.trend.data[0].series[offset].x;
}

// チャートにデータセットの設定とオプション設定
function refreshChart(chartData) {
    chart.config.data.datasets = chartData.datasets;
    chart.config.options = {
        plugins: {
            title: { display: false, },
            legend: {
                position: "top",
                align: "center",
                labels: {
                    filter: function (item) {
                        // console.log(item);
                        //ローソク足とvolumeの凡例を非表示
                        if (item.datasetIndex === 0 || item.text === 'volume')
                            return false;
                        else
                            return true;
                    },
                    boxWidth: 6,
                    boxHeight: 2,
                },
            },
        },
        // 後でアクセスするので
        scales: { x: {} },
    };

    if (chart.config.data.datasets[1].label == 'volume') {
        //データセットから出来高max取得
        const volume_max = chart.config.data.datasets[1].data.map((d) => { return d.y}).reduce(function (a, b) {return Math.max(a, b)});
        chart.config.options.scales.yAxisSub = {
            suggestedMax: volume_max * 3,
            position: "right",
            ticks: {
                callback: function (value, index) {
                    if (value > volume_max || value < (volume_max * 2) / 4 || value == 0)
                        return "";
                    else return `${(value / 10000).toLocaleString()}万`;
                },
                mirror: true,
            },
            grid: {
                drawOnChartArea: false,
            },
        };
    }
    chart.update();
}
```


```Javascript:chart-trends.js
// luxonから曜日を取得
function DayOfWeek(dt){     // refer to https://stackoverflow.com/questions/69678623/luxon-js-get-day-from-week-day-number
    const luxonDay = (dt + 6) % 7;  //剰余
    return luxonDay;    // 0:Mon 4:Fri
}

function getWeekSeries(dailydata){
    let series = [];
    let series_volume = [];
    let d = luxon.DateTime.fromMillis(dailydata[0].x);
    let d_from = d;
    let date_next_sunday = d.plus({ days: 6 - DayOfWeek(d) });
    let open = dailydata[0].o;
    let high = dailydata[0].h;
    let low = dailydata[0].l;
    let close = dailydata[0].c;
    let volume = dailydata[0].v;
    for (let i = 1; i < dailydata.length; i++) {
        let dt = luxon.DateTime.fromMillis(dailydata[i].x);
        isNextWeek = dt > date_next_sunday;
        if(!isNextWeek || i == dailydata.length - 1){
            if(dailydata[i].h > high)
                high = dailydata[i].h;
            if(dailydata[i].l < low)
                low = dailydata[i].l;
            close = dailydata[i].c;
            volume += dailydata[i].v;
            if(i == dailydata.length - 1)
                d = dt;
        }
        if(i == dailydata.length - 1 || isNextWeek){
            data = {
                    x: d.valueOf(),
                    d: d.toFormat('yyyy-MM-dd'),
                    o: open,
                    h: high,
                    l: low,
                    c: close,
                    v: volume,
                };
                series.push(data);
            d = dt;
            d_from = d;
            open = dailydata[i].o;
            high = dailydata[i].h;
            low = dailydata[i].l;
            close = dailydata[i].c;
            volume = dailydata[i].v;
        }
        d = dt;
        date_next_sunday = d.plus({ days: 6 - DayOfWeek(d) });
    }
        return series;                
}

function getMonthSeries(dailydata){
    let series = [];
    let series_volume = [];
    let n = dailydata.length;
    let d = luxon.DateTime.fromMillis(dailydata[0].x);
    let d_from = d;
    let open = dailydata[0].o;
    let high = dailydata[0].h;
    let low = dailydata[0].l;
    let close = dailydata[0].c;
    let volume = dailydata[0].v;
    for (let i = 1; i < dailydata.length; i++) {
        let dt = luxon.DateTime.fromMillis(dailydata[i].x);
        isNextMonth = dt.month != d.month;
        if(!isNextMonth || i == n - 1){
            if(dailydata[i].h > high)
                high = dailydata[i].h;
            if(dailydata[i].l < low)
                low = dailydata[i].l;
            close = dailydata[i].c;
            volume += dailydata[i].v;
            if(i == n - 1)
                d = dt;
        }
        if(i == n - 1 || isNextMonth){
            data = {
                    x: d.valueOf(),
                    d: d.toFormat('yyyy-MM-dd'),
                    o: open,
                    h: high,
                    l: low,
                    c: close,
                    v: volume,
                };
                series.push(data);
            d = dt;
            d_from = d;
            open = dailydata[i].o;
            high = dailydata[i].h;
            low = dailydata[i].l;
            close = dailydata[i].c;
            volume = dailydata[i].v;
        }
        d = dt;
    }
    return series;                
}

// ------------ 移動平均
function calcMa(series_source, interval, set_null = true){
    let series = [];
    let sum = 0;
    for (let i = 0; i < series_source.length; i++) {
        sum += 'y' in series_source[i] ? series_source[i].y :series_source[i].c;
        if (i >= interval - 1 ){
            if (i >= interval)
                sum -= 'y' in series_source[i] ? series_source[i - interval].y :series_source[i - interval].c;
            
            const ma = sum / interval;
            data = {
                x: series_source[i].x,
                y: ma,
            }
            series.push(data);
        }else{
            if(set_null){
                series.push({x: series_source[i].x, y: null });
            }
        }
    }
    return series;
}

// --------------- 移動平均乖離率
function calcKairi(data){
    let mas = [];
    if (data.trend.name == 'MA'){
        mas = data.trend.data;
    }else{
        for(let i = 0; i < data.trend.data.length ; i++){
            let ma = calcMa(data.candle, data.trend.data[i].params, true);
            mas.push({label: 'MA' + data.trend.data[i].params, interval: data.trend.data[i].params, series: ma});
        }

    }
    let kairis = [];
    for(let i = 0; i < data.trend.data.length; i++){
        series = [];
        for(let j = 0; j<mas[i].series.length;j++){
            const kairi = mas[i].series[j].y != null ? (data.candle[j].c - mas[i].series[j].y) / mas[i].series[j].y * 100 : null;
            series.push({ x: mas[i].series[j].x, y: kairi});
        }
        kairis[i] = {label: mas[i].label, params: [mas[i].interval], series: series}
    }
    return kairis;
}

// 小次郎の大循環ステージ
function calcStage(series_source, params){
    let mas = [];
    for(let i = 0; i < params.length; i++){
        let ma = calcMa(series_source, params[i], true);
            mas.push({label: 'MA' + params[i], interval: params[i], series: ma});
    }
    let series_stage = [];
    for(let i = 0; i < mas[0].series.length; i++){
        let ma = [];
        for(let j = 0; j < params.length; j++)
            ma[j] = {param: params[j], y: i >= params[2] ? mas[j].series[i].y : null};
        let stage = 0;
        if(mas[0].series[i].y > mas[1].series[i].y && mas[1].series[i].y > mas[2].series[i].y)
            stage = 1;
        else if(mas[1].series[i].y > mas[0].series[i].y && mas[0].series[i].y > mas[2].series[i].y)
            stage = 2;
        else if(mas[1].series[i].y > mas[2].series[i].y && mas[2].series[i].y > mas[0].series[i].y)
            stage = 3;
        else if(mas[2].series[i].y > mas[1].series[i].y && mas[1].series[i].y > mas[0].series[i].y)
            stage = 4;
        else if(mas[2].series[i].y > mas[0].series[i].y && mas[0].series[i].y > mas[1].series[i].y)
            stage = 5;
        else if(mas[0].series[i].y > mas[2].series[i].y && mas[2].series[i].y > mas[1].series[i].y)
            stage = 6;
        series_stage.push({x: mas[0].series[i].x, y: stage > 0 ? stage : null, short: ma[0], middle: ma[1], long: ma[2],});
    }
    return [{label: 'stage', series: series_stage}]
}

// --------------- 一目均衡表
function calcIchi(series_source){
    const ts_day = 86400000; //TimeSpanの1日相当
    let t = [];
    let k = [];
    let c = [];
    let m52 = [];
    let span1 = [];
    let span2 = [];
    for (let i = 0; i < series_source.length + 25; i++){
        let tenkan = null;
        let kijun = null;
        let chikou = null;
        let s2 = null;

        let x = i < series_source.length ? series_source[i].x : series_source[series_source.length - 1].x + (i - series_source.length + 1) * ts_day;
        if(i >= series_source.length){
            t[i] = { x:x, y:null};
            k[i] = { x:x, y:null};
            c[i] = { x:x, y:null};
        }else{
            let h = series_source[i].h;
            let l = series_source[i].l;
            if ( i >= 25 ){
                for (let j = 1; j < 26 ; j++){
                    if (series_source[i - j].h > h)
                        h = series_source[i - j].h;
                    if (series_source[i - j].l < l)
                        l = series_source[i - j].l;
                    if ( j == 8)
                        tenkan = (h + l) / 2;
                }
                kijun = (h + l ) / 2;
            }
            if(i >= 51){
                for (let j = 26; j < 52 ; j++){
                    if (series_source[i - j].h > h)
                        h = series_source[i - j].h;
                    if (series_source[i - j].l < l)
                        l = series_source[i - j].l;
                }
                s2 = (h + l)/2;
            }
            m52[i] = s2;
            t[i] = { x:x, y:tenkan};
            k[i] = { x:x, y:kijun};
            if(series_source.length - i > 26)
                c[i] = { x:x, y:series_source[i + 26].c};
            else
                c[i] = { x:x, y:null};
        }
        if(i >= 51){
            span1[i] = { x:x, y:((t[i - 25].y + k[i - 25].y) / 2)};
        }
        else
            span1[i] = { x:x, y:null};
        if(i >= 77)
            span2[i] = { x:x, y:m52[i - 26]};
        else
            span2[i] = { x:x, y:null};

    }
    return [
        {label: '転換線', series: t},
        {label: '基準線', series: k},
        {label: '遅行線', series: c},
        {label: '先行スパン1', series: span1},
        {label: '先行スパン2', series: span2},
    ]
    // data.seriesに26日間のnullデータ追加必要
}

// refer to https://stackoverflow.com/questions/40057020/calculating-exponential-moving-average-ema-using-javascript
function calcEma(mArray,mRange) {
    var k = 2/(mRange + 1);
    emaArray = [mArray[0]];
    for (var i = 1; i < mArray.length; i++) {
        emaArray.push(mArray[i] * k + emaArray[i - 1] * (1 - k));
    }
    return emaArray;
}

// ------------------- MACD
function calcMacd(series_source, params){
    const array_y = series_source.map(function (item) {
        return item['c'];
    })
    const array_x = series_source.map(function (item) {
        return item['x'];
    })
    ema =[calcEma(array_y, params[0]), calcEma(array_y, params[1])]
    macd = [];
    for(let i = 0;i<ema[0].length;i++){
        y_value = isNaN(ema[0][i]) ? null :  ema[0][i] - ema[1][i];
        data = {x:array_x[i], y: y_value};
            macd.push(data);
    }
    signal = calcMa(macd, params[2], true);
    return [{label: 'MACD', params:[params[0], params[1]], series: macd}, {label: 'Signal', param: params[2], series: signal}]
}

// ------------------ RSI Wilder:引数なし
function calcRsi(series_source, params, simple = false){
    let result = [];
    let up = 0;
    let down = 0;
    let up_prev = 0;
    let down_prev = 0;
    interval = params[0];
    result[0] = {x: series_source[0].x, y: null};
    for (let i = 1; i < series_source.length; i++){
        const diff = series_source[i].c - series_source[i - 1].c;
        if(i < interval){
            up += diff > 0 ? diff : 0;
            down -= diff < 0 ? diff : 0;
            result[i] = {x: series_source[i].x, y: null};
        }else{
            up = ((i == interval) ? up : (simple ? up : up_prev * (interval - 1))) + (diff > 0 ? diff : 0);
            down = ((i == interval) ? down : (simple ? down : down_prev * (interval - 1))) - (diff < 0 ? diff : 0);
            if(simple && i > interval){
                const diff2 = series_source[i - interval].c - series_source[i - interval -1].c;
                if(diff2 > 0)
                    up -= diff2;
                else
                    down += diff2;
            }
            result[i] = {x: series_source[i].x, y: up / (up + down) * 100};
            up_prev = up / interval;
            down_prev = down / interval;
        }
    }
    const series_signal = calcMa(result, params[1], true);
    let x_from = series_signal[params[0] + params[1]].x;
    return [{label: simple ? 'Simple' : 'Wilder', param: params[0], series: result}, 
        {label: 'Signal', param: params[1], series: series_signal.map(d => {return {x:d.x, y: d.x < x_from ? null: d.y}})}];
}

// refer to https://www.tcom242242.net/entry/fx-begin/compute-rci-by-python/
// --------------------- RCI   微妙に数値が合っていないかも
function calcRci(series_source, param){
    let array_days = [];  //seriesが昇順なので
        for(let j = param; j > 0; j--)
            array_days.push(j);
    let rci_list = [];
    for(let i = 0; i < series_source.length; i++){
        if (i < param){
            rci_list[i] = {x:series_source[i].x, y:null};
            continue;
        }
        let closes = series_source.filter((value, range) => range > i - param && range <= i).map(d => { return d.c})
        // refer to https://qiita.com/nevius5829/items/b78f5428b7086ce1e644
        const sorted = closes.slice().sort((a,b) => b - a); 
        const rank = closes.slice().map((item) => { return sorted.indexOf(item) + 1 })
        let diff2 = [];
        for (let j = 0; j < rank.length;j++){
            diff2[j] = Math.pow(array_days[j] - rank[j], 2);
        }
        const sum_diff = diff2.reduce((a, b) => { return a + b; });
        const rci = (1 - ((6 * sum_diff) / (Math.pow(param, 3) - param))) * 100
        rci_list.push({x:series_source[i].x, y: rci});
    }
    return {label: `RCI${param}`, param: param, series: rci_list};
}

// ------------------ パラボリックSAR
function calcSar(series_source, params){
    // params = [0.02, 0.02, 0.2];
    let trend = null;
    let sars = [{x: series_source[0].x, y: null}];
    let trends = [{x: series_source[0].x, y: null}];
    ep = null;
    af = params[0];
    sar = null;
    for (let i = 1; i < series_source.length; i++){
        if (trend == null){
            if(series_source[i].h > series_source[i - 1].h && series_source[i].l > series_source[i - 1].l)
            {    trend = 1;
                ep = series_source[i].h;
                sar = series_source[i - 1].l;
            }
            else if(series_source[i].h < series_source[i - 1].h && series_source[i].l < series_source[i - 1].l)
            {    trend = -1;
                ep = series_source[i].l;
                sar = series_source[i - 1].h;
            }
        }else{
            if((trend > 0 && series_source[i].l < sar) || (trend < 0 && series_source[i].l > sar)){
                //トレンド転換
                sar = ep;
                ep = trend > 0 ?series_source[i].l : series_source[i].h;
                af = params[0];
                trend = - trend;
            } else {
                sar = sar + af * ( ep - sar );
                if( (trend > 0 && ep < series_source[i].h) || (trend < 0 && ep > series_source[i].l )){
                    if(af < params[2])
                        af += params[1];
                }
                if(trend > 0){
                    sar = Math.min(sar, series_source[i - 1].l, series_source[i - 2].l);
                    ep = Math.max(ep, series_source[i].h);
                }else{
                    sar = Math.max(sar, series_source[i - 1].h, series_source[i - 2].h);
                    ep = Math.min(ep, series_source[i].l);
                }
            }
        }
        sars.push({x: series_source[i].x, y: sar, trend: trend});
    }
    return {label: 'Parabolic', param: params, series: sars}
}

function calcStd(series_source, m){
    let sum = 0;
    for(let i = 0; i< series_source.length;i++){
        sum += Math.pow(series_source[i].c - m, 2);
    }
    return Math.sqrt(sum / (series_source.length));
}

// ----------------- ボリンジャーバンド
function calcBollinger(series_source, params){
    s = [[],[],[],[],[],[], []];
    ma = calcMa(series_source, params[0], true);
    s[0] = ma;
    for(let i = 0; i < ma.length; i++){
        sd = null;
        if (ma[i].y != null){
            series = series_source.filter(
                (value, index) => 
                    (index > i - params[0] && index <= i)
            )
            sd = calcStd(series, ma[i].y);
            for (let j = 0; j < 3; j++){
                s[j * 2 + 1][i] = {x: ma[i].x, y: ma[i].y + sd * (j + 1)};
                s[j * 2 + 2][i] = {x: ma[i].x, y: ma[i].y - sd * (j + 1)};
                }
        }else{
            for (let j = 0; j < 6; j++){
                s[j + 1][i] = {x: ma[i].x, y: null};
            }
        }

    }
    bb = [];
    labels = ['MA' + params[0], '+1σ', '-1σ','+2σ', '-2σ','+3σ', '-3σ']
    for(let i = 0; i < 7; i++){
        bdata = {label: labels[i], params: params[0], series: s[i]}
        bb[i] = bdata;
    }
    return bb;
}
```

```Javascript:chart-writeTable.js
// ----------------------- 表示中のチャートデータをテーブル表示
function writeTable(checkbox) {
    document.getElementById("price").innerHTML = "";
    if (!checkbox.checked) {
        return;
    }
    reverse = true;
    const tr_head = document.createElement("tr");
    head =
        "<tr><th>date</th><th>open</th><th>high</th><th>low</th><th>close</th><th>volume</th>";
    if (chartData.trend !== undefined) {
        for (let j = 0; j < chartData.trend.data.length; j++) {
        head += `<th>${chartData.trend.data[j].label}</th>`;
        }
    }
    if (chartData.oscillator !== undefined) {
        for (let j = 0; j < chartData.oscillator.data.length; j++) {
        head += `<th>${chartData.oscillator.data[j].label}</th>`;
        }
    }
    head += "</tr>";
    tr_head.innerHTML = head;
    document.getElementById("price").insertAdjacentElement("beforeend", tr_head);
    if (reverse) {
        for (let i = chartData.candle.length - 1; i >= 0; i--) {
        append_row(i);
        }
    } else {
        for (let i = 0; i < chartData.candle.length; i++) {
        append_row(i);
        }
    }
}

function append_row(i) {
    const tr = document.createElement("tr");
    let row = chartData.candle[i];
    let tds = [
        luxon.DateTime.fromMillis(row.x).setLocale("ja").toFormat("y-M-d EEE"),
        Number(row.o).toLocaleString(),
        Number(row.h).toLocaleString(),
        Number(row.l).toLocaleString(),
        Number(row.c).toLocaleString(),
        Number(row.v).toLocaleString(),
    ];

    if (chartData.trend !== undefined) {
    for (let j = 0; j < chartData.trend.data.length; j++) {
        value = chartData.trend.data[j].series[i].y;
        tds.push(value !== null ? (Math.round(value * 100) / 100).toLocaleString() : "");
    }
    }
    if (chartData.oscillator !== undefined) {
    for (let j = 0; j < chartData.oscillator.data.length; j++) {
        value = chartData.oscillator.data[j].series[i].y;
        tds.push(value !== null ? (Math.round(value * 100) / 100).toLocaleString() : "");
    }
    }
    for (let j = 0; j < tds.length; j++) {
        td = document.createElement("td");
        td.textContent = tds[j];
        tr.insertAdjacentElement("beforeend", td);
    }
    document.getElementById("price").insertAdjacentElement("beforeend", tr);
}
```
- chart-csv-reader.js ：　「[株価チャート chartjs-chart-financial を試行錯誤で使ってみた　2](https://qiita.com/grayhead0603/items/c6a966de0d42c8589274)」と変更ありません。

その他参考にしたページ[^5][^6][^7][^8][^9][^10]。

![2024-03-02 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182396/92fb8d58-090c-0d37-548d-465525f1b693.png)


## 参考ページ

[^1]: Chart.js ドキュメント [Color formats](https://www.chartjs.org/docs/latest/general/colors.html#color-formats)
[^2]: Chart.js ドキュメント [Indexable Options](https://www.chartjs.org/docs/latest/general/options.html)
[^3]: Chart.js ドキュメント [Scriptable Options](https://www.chartjs.org/docs/latest/general/options.html#scriptable-options)
[^4]: Chart.js ドキュメント [Filling modes](https://www.chartjs.org/docs/latest/charts/area.html)
[^5]: [JavaScript | ラジオボタン（radio）の切り替えイベントを実装する方法](https://1-notes.com/javascript-radio-button-event/)
[^6]: [Web ビューアで JavaScript を使用したスクリプト作成](https://help.claris.com/ja/pro-help/content/scripting-javascript-in-web-viewers.html)
[^7]: [JavaScript 要素を表示/非表示にするサンプル(display と visibility)](https://itsakura.com/javascript-display)
[^8]: [JavaScript のコードから要素のスタイルを変更する : JavaScript](https://www.ipentec.com/document/javascript-change-element-style)
[^9]: [JavaScript で名前付き引数を使いこなす 5 つの方法と 10 個のサンプルコード](https://jp-seemore.com/web/3904/)
[^10]: [CSSで横並びレイアウトを実現簡単にするinline-blockとは？](https://www.sejuku.net/blog/52822)