はじめに

plotly/plotly.js で、ズームやパン(移動)と連動して Y軸の表示範囲を自動で調整(オートスケール)する方法

TL;DR

  • ズームや移動時は plotly_relayout イベントが発火
  • .on('plotly_relayout', handler) でハンドラを設定
  • 表示する Y 軸範囲を決定し relayout を実行
  • Plotly.relayout()plotly_relayout イベントが発火するので無限ループにならないように注意
この記事が参考になった方
ここここからチャージや購入してくれると嬉しいです(ブログ主へのプレゼントではなく、ご自身へのチャージ)
欲しいもの / Wish list

目次

  1. はじめに
  2. TL;DR
  3. 環境・条件
  4. 詳細
    1. 対象チャート
    2. 方法
    3. 実装サンプル
  5. まとめ
  6. 参考文献

環境・条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.7
BuildVersion: 19H2

$ node -v
v12.7.0

$ npm -v
6.14.5

$ npm ls vue plotly.js-dist
├── plotly.js-dist@1.56.0
└── vue@2.6.11

詳細

対象チャート

この記事では Candlestick ChartsSimple Candlestick Chart をベースにしている。他のチャート形式でも同じようなやり方で対応できるはず。

方法

参考:


ズームや移動時は plotly_relayout イベントが発火するので、.on('plotly_relayout', handler) でハンドラを設定する。

Candlestick チャートの場合、ズーム時と移動時で(なぜか)データ形式が異なるので、どちらなのか判別する処理が必要になる。(他チャートでも同じようなことになるのかは不明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Plotly from 'plotly.js-dist';
Plotly.newPlot('my-plotly', data, layout);
document.getElementById('my-plotly').on('plotly_relayout', eventData => console.log(eventData));
// ***** ズーム時 *****
// => {
// "xaxis.range[0]": "2017-01-15 17:15:29.2842",
// "xaxis.range[1]": "2017-01-30 10:57:31.6269",
// }

// ***** 移動時 *****
// => {
// "xaxis.range": [
// "2017-01-15 19:29:48.2864",
// "2017-01-30 13:11:50.6291",
// ]
// }

上記データからどの範囲のデータが表示されているかを算出可能なので、イベントハンドラ内で Y軸範囲を算出して relayout を実行すれば Y 軸の表示範囲も変更できる。

1
2
3
4
5
document.getElementById('my-plotly').on('plotly_relayout', eventData => {
// 色々と処理(X軸範囲からプロットされているデータを抽出したり、範囲内でのY軸最大/最小を求めたり)
// ...
Plotly.relayout('my-plotly', 'yaxis.range', [yMin, yMax]);
});

Plotly.relayout() を実行すると (当然だが) plotly_relayout イベントが発火するので、フラグなどで制御しないと無限ループになるので注意。

1
2
3
4
5
6
7
8
9
10
11
12
// とても雑な例
let flag = false;
document.getElementById('my-plotly').on('plotly_relayout', eventData => {
if (flag) {
flag = false;
return;
}

// ...
flag = true;
Plotly.relayout('my-plotly', 'yaxis.range', [yMin, yMax]);
});

実装サンプル

Vue.js での実装サンプル。

簡単な流れは以下(基本的に前章の通り)で、異常値にならないようにガードをしている。

  • イベントデータから X 軸の対応するデータを抽出
  • 範囲内での Y 軸の最大値, 最小値を算出
  • Plotly.relayout() で Y 軸の表示範囲を調整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
<template>
<div id="my-plotly"></div>
</template>

<script>
import Plotly from 'plotly.js-dist';
import moment from 'moment';

export default {
data() {
return {
plot: null,
relayouting: false,
x: [],
close: [],
high: [],
low: [],
open: [],
};
},
created() {
this.generateData();
},
async mounted() {
const trace1 = {
x: this.x,
close: this.close,
high: this.high,
low: this.low,
open: this.open,
decreasing: { line: { color: '#f77c62' }, fillcolor: '#f77c62' },
increasing: { line: { color: '#3fd3a4' }, fillcolor: '#3fd3a4' },
line: { color: 'rgb(105, 124, 116, 0.7)' },
type: 'candlestick',
xaxis: 'x',
yaxis: 'y',
};
const data = [trace1];

const layout = {
dragmode: 'zoom',
xaxis: {
autorange: true,
domain: [0, 1],
range: ['2017-01-03 12:00', '2017-02-15 12:00'],
rangeslider: { range: ['2017-01-03 12:00', '2017-02-15 12:00'] },
},
yaxis: {
autorange: true,
domain: [0, 1],
range: [114.609999778, 137.410004222],
type: 'linear',
},
};

this.plot = await Plotly.newPlot('my-plotly', data, layout);

// ズーム/移動 時のイベントハンドラ設定
this.plot.on('plotly_relayout', this.plotlyRelayoutHandler);
},
methods: {
plotlyRelayoutHandler(eventData) {
if (this.relayouting) {
this.relayouting = !this.relayouting;
return;
}

const [yMin, yMax] = this.calcYMinAndYMax(eventData);
Plotly.relayout('my-plotly', 'yaxis.range', [yMin, yMax]);

this.relayouting = !this.relayouting;
},
extractSelectedXRange(eventData) {
if ('xaxis.range' in eventData) {
// Pan(移動) のときは Array
const selectedTimeMin = moment(eventData['xaxis.range'][0]).unix();
const selectedTimeMax = moment(eventData['xaxis.range'][1]).unix();
return [selectedTimeMin, selectedTimeMax];
} else {
// Zoom のときは Object
const selectedTimeMin = moment(eventData['xaxis.range[0]']).unix();
const selectedTimeMax = moment(eventData['xaxis.range[1]']).unix();
return [selectedTimeMin, selectedTimeMax];
}
},
findXIndexesInRange(eventData) {
const [selectedTimeMin, selectedTimeMax] = this.extractSelectedXRange(eventData);
const xMinIndex =
this.x.findIndex((x, i) => {
return moment(x).unix() > selectedTimeMin;
}) || 0;
const xMaxIndex =
this.x.findIndex((x, i) => {
return moment(x).unix() > selectedTimeMax;
}) || this.x.length - 1;
return [xMinIndex, xMaxIndex];
},
calcYMinAndYMax(eventData) {
const [xMinIndex, xMaxIndex] = this.findXIndexesInRange(eventData);
let yMin = Math.min(...this.low.slice(xMinIndex, xMaxIndex));
if (yMin === Infinity || yMin === -Infinity) {
yMin = Math.min(...this.low);
}
let yMax = Math.max(...this.high.slice(xMinIndex, xMaxIndex));
if (yMax === Infinity || yMax === -Infinity) {
yMax = Math.max(...this.high);
}
return [yMin, yMax];
},
// candlestick チャートのデータ生成
generateData() {
this.x = [
'2017-01-04',
'2017-01-05',
'2017-01-06',
'2017-01-09',
'2017-01-10',
'2017-01-11',
'2017-01-12',
'2017-01-13',
'2017-01-17',
'2017-01-18',
'2017-01-19',
'2017-01-20',
'2017-01-23',
'2017-01-24',
'2017-01-25',
'2017-01-26',
'2017-01-27',
'2017-01-30',
'2017-01-31',
'2017-02-01',
'2017-02-02',
'2017-02-03',
'2017-02-06',
'2017-02-07',
'2017-02-08',
'2017-02-09',
'2017-02-10',
'2017-02-13',
'2017-02-14',
'2017-02-15',
];
this.close = [
116.019997,
116.610001,
117.910004,
118.989998,
119.110001,
119.75,
119.25,
119.040001,
120,
119.989998,
119.779999,
120,
120.080002,
119.970001,
121.879997,
121.940002,
121.949997,
121.629997,
121.349998,
128.75,
128.529999,
129.080002,
130.289993,
131.529999,
132.039993,
132.419998,
132.119995,
133.289993,
135.020004,
135.509995,
];
this.high = [
116.510002,
116.860001,
118.160004,
119.43,
119.379997,
119.93,
119.300003,
119.620003,
120.239998,
120.5,
120.089996,
120.449997,
120.809998,
120.099998,
122.099998,
122.440002,
122.349998,
121.629997,
121.389999,
130.490005,
129.389999,
129.190002,
130.5,
132.089996,
132.220001,
132.449997,
132.940002,
133.820007,
135.089996,
136.270004,
];
this.low = [
115.75,
115.809998,
116.470001,
117.940002,
118.300003,
118.599998,
118.209999,
118.809998,
118.220001,
119.709999,
119.370003,
119.730003,
119.769997,
119.5,
120.279999,
121.599998,
121.599998,
120.660004,
120.620003,
127.010002,
127.779999,
128.160004,
128.899994,
130.449997,
131.220001,
131.119995,
132.050003,
132.75,
133.25,
134.619995,
];
this.open = [
115.849998,
115.919998,
116.779999,
117.949997,
118.769997,
118.739998,
118.900002,
119.110001,
118.339996,
120,
119.400002,
120.449997,
120,
119.550003,
120.419998,
121.669998,
122.139999,
120.93,
121.150002,
127.029999,
127.980003,
128.309998,
129.130005,
130.539993,
131.350006,
131.649994,
132.460007,
133.080002,
133.470001,
135.520004,
];
},
},
};
</script>

初期状態


ズーム時(Y軸範囲の調整あり)


ズーム時(Y軸範囲の調整なし = デフォルト)

まとめ

  • ズームや移動時は plotly_relayout イベントが発火
  • .on('plotly_relayout', handler) でハンドラを設定
  • 表示する Y 軸範囲を決定し relayout を実行
  • Plotly.relayout()plotly_relayout イベントが発火するので無限ループにならないように注意

参考文献

関連記事

この記事が参考になった方
ここここからチャージや購入してくれると嬉しいです(ブログ主へのプレゼントではなく、ご自身へのチャージ)
欲しいもの / Wish list