hyperprop-charting-library 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperprop-charting-library.cjs +711 -16
- package/dist/hyperprop-charting-library.d.ts +49 -1
- package/dist/hyperprop-charting-library.js +711 -16
- package/dist/index.cjs +711 -16
- package/dist/index.d.cts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +711 -16
- package/docs/API.md +28 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -145,8 +145,476 @@ var DEFAULT_OPTIONS = {
|
|
|
145
145
|
labelTextColor: "#0b1220",
|
|
146
146
|
labelBorderRadius: 3
|
|
147
147
|
},
|
|
148
|
-
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
148
|
+
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
149
|
+
indicators: []
|
|
149
150
|
};
|
|
151
|
+
var getIndicatorSourceValue = (point, source) => {
|
|
152
|
+
if (source === "open") return point.o;
|
|
153
|
+
if (source === "high") return point.h;
|
|
154
|
+
if (source === "low") return point.l;
|
|
155
|
+
if (source === "hl2") return (point.h + point.l) / 2;
|
|
156
|
+
return point.c;
|
|
157
|
+
};
|
|
158
|
+
var clampIndicatorLength = (value, fallback) => {
|
|
159
|
+
return Math.max(1, Math.round(Number(value) || fallback));
|
|
160
|
+
};
|
|
161
|
+
var computeSmaSeries = (data, length, source) => {
|
|
162
|
+
const result = new Array(data.length).fill(null);
|
|
163
|
+
let sum = 0;
|
|
164
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
165
|
+
const point = data[i];
|
|
166
|
+
if (!point) continue;
|
|
167
|
+
const value = getIndicatorSourceValue(point, source);
|
|
168
|
+
sum += value;
|
|
169
|
+
if (i >= length) {
|
|
170
|
+
const oldPoint = data[i - length];
|
|
171
|
+
if (oldPoint) {
|
|
172
|
+
sum -= getIndicatorSourceValue(oldPoint, source);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (i >= length - 1) {
|
|
176
|
+
result[i] = sum / length;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
};
|
|
181
|
+
var computeEmaSeries = (data, length, source) => {
|
|
182
|
+
const result = new Array(data.length).fill(null);
|
|
183
|
+
const alpha = 2 / (length + 1);
|
|
184
|
+
let prev = null;
|
|
185
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
186
|
+
const point = data[i];
|
|
187
|
+
if (!point) continue;
|
|
188
|
+
const value = getIndicatorSourceValue(point, source);
|
|
189
|
+
prev = prev === null ? value : alpha * value + (1 - alpha) * prev;
|
|
190
|
+
if (i >= length - 1) {
|
|
191
|
+
result[i] = prev;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
var computeRmaSeries = (data, length, source) => {
|
|
197
|
+
const result = new Array(data.length).fill(null);
|
|
198
|
+
let prev = null;
|
|
199
|
+
let seedSum = 0;
|
|
200
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
201
|
+
const point = data[i];
|
|
202
|
+
if (!point) continue;
|
|
203
|
+
const value = getIndicatorSourceValue(point, source);
|
|
204
|
+
if (i < length) {
|
|
205
|
+
seedSum += value;
|
|
206
|
+
if (i === length - 1) {
|
|
207
|
+
prev = seedSum / length;
|
|
208
|
+
result[i] = prev;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
prev = prev === null ? value : (prev * (length - 1) + value) / length;
|
|
213
|
+
result[i] = prev;
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
var computeWmaSeriesFromValues = (values, length) => {
|
|
218
|
+
const result = new Array(values.length).fill(null);
|
|
219
|
+
const denominator = length * (length + 1) / 2;
|
|
220
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
221
|
+
if (i < length - 1) continue;
|
|
222
|
+
let weighted = 0;
|
|
223
|
+
let valid = true;
|
|
224
|
+
for (let j = 0; j < length; j += 1) {
|
|
225
|
+
const value = values[i - length + 1 + j];
|
|
226
|
+
if (value == null) {
|
|
227
|
+
valid = false;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
weighted += value * (j + 1);
|
|
231
|
+
}
|
|
232
|
+
if (valid) {
|
|
233
|
+
result[i] = weighted / denominator;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
};
|
|
238
|
+
var computeWmaSeries = (data, length, source) => {
|
|
239
|
+
const sourceValues = data.map((point) => getIndicatorSourceValue(point, source));
|
|
240
|
+
return computeWmaSeriesFromValues(sourceValues, length);
|
|
241
|
+
};
|
|
242
|
+
var computeVwmaSeries = (data, length, source) => {
|
|
243
|
+
const result = new Array(data.length).fill(null);
|
|
244
|
+
let sumPv = 0;
|
|
245
|
+
let sumV = 0;
|
|
246
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
247
|
+
const point = data[i];
|
|
248
|
+
if (!point) continue;
|
|
249
|
+
const sourceValue = getIndicatorSourceValue(point, source);
|
|
250
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
251
|
+
sumPv += sourceValue * volume;
|
|
252
|
+
sumV += volume;
|
|
253
|
+
if (i >= length) {
|
|
254
|
+
const oldPoint = data[i - length];
|
|
255
|
+
if (oldPoint) {
|
|
256
|
+
const oldSource = getIndicatorSourceValue(oldPoint, source);
|
|
257
|
+
const oldVolume = Math.max(0, oldPoint.v ?? 0);
|
|
258
|
+
sumPv -= oldSource * oldVolume;
|
|
259
|
+
sumV -= oldVolume;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (i >= length - 1 && sumV > 0) {
|
|
263
|
+
result[i] = sumPv / sumV;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
};
|
|
268
|
+
var computeHmaSeries = (data, length, source) => {
|
|
269
|
+
const halfLength = Math.max(1, Math.floor(length / 2));
|
|
270
|
+
const sqrtLength = Math.max(1, Math.round(Math.sqrt(length)));
|
|
271
|
+
const wmaHalf = computeWmaSeries(data, halfLength, source);
|
|
272
|
+
const wmaFull = computeWmaSeries(data, length, source);
|
|
273
|
+
const diff = wmaHalf.map((value, idx) => {
|
|
274
|
+
const full = wmaFull[idx];
|
|
275
|
+
if (value == null || full == null) return null;
|
|
276
|
+
return 2 * value - full;
|
|
277
|
+
});
|
|
278
|
+
return computeWmaSeriesFromValues(diff, sqrtLength);
|
|
279
|
+
};
|
|
280
|
+
var computeStdDevSeries = (data, length, source) => {
|
|
281
|
+
const result = new Array(data.length).fill(null);
|
|
282
|
+
const values = data.map((point) => point ? getIndicatorSourceValue(point, source) : 0);
|
|
283
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
284
|
+
if (i < length - 1) continue;
|
|
285
|
+
let sum = 0;
|
|
286
|
+
for (let j = 0; j < length; j += 1) {
|
|
287
|
+
sum += values[i - j] ?? 0;
|
|
288
|
+
}
|
|
289
|
+
const mean = sum / length;
|
|
290
|
+
let variance = 0;
|
|
291
|
+
for (let j = 0; j < length; j += 1) {
|
|
292
|
+
const delta = (values[i - j] ?? 0) - mean;
|
|
293
|
+
variance += delta * delta;
|
|
294
|
+
}
|
|
295
|
+
result[i] = Math.sqrt(variance / length);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
299
|
+
var computeRsiSeries = (data, length) => {
|
|
300
|
+
const result = new Array(data.length).fill(null);
|
|
301
|
+
if (data.length < 2) return result;
|
|
302
|
+
let avgGain = 0;
|
|
303
|
+
let avgLoss = 0;
|
|
304
|
+
let seeded = false;
|
|
305
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
306
|
+
const point = data[i];
|
|
307
|
+
const prevPoint = data[i - 1];
|
|
308
|
+
if (!point || !prevPoint) continue;
|
|
309
|
+
const delta = point.c - prevPoint.c;
|
|
310
|
+
const gain = Math.max(0, delta);
|
|
311
|
+
const loss = Math.max(0, -delta);
|
|
312
|
+
if (i <= length) {
|
|
313
|
+
avgGain += gain;
|
|
314
|
+
avgLoss += loss;
|
|
315
|
+
if (i === length) {
|
|
316
|
+
avgGain /= length;
|
|
317
|
+
avgLoss /= length;
|
|
318
|
+
seeded = true;
|
|
319
|
+
}
|
|
320
|
+
} else if (seeded) {
|
|
321
|
+
avgGain = (avgGain * (length - 1) + gain) / length;
|
|
322
|
+
avgLoss = (avgLoss * (length - 1) + loss) / length;
|
|
323
|
+
}
|
|
324
|
+
if (seeded && i >= length) {
|
|
325
|
+
if (avgLoss === 0) {
|
|
326
|
+
result[i] = 100;
|
|
327
|
+
} else {
|
|
328
|
+
const rs = avgGain / avgLoss;
|
|
329
|
+
result[i] = 100 - 100 / (1 + rs);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
};
|
|
335
|
+
var computeAtrSeries = (data, length) => {
|
|
336
|
+
const result = new Array(data.length).fill(null);
|
|
337
|
+
if (data.length === 0) return result;
|
|
338
|
+
const trueRanges = [];
|
|
339
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
340
|
+
const point = data[i];
|
|
341
|
+
if (!point) continue;
|
|
342
|
+
const prevPoint = i > 0 ? data[i - 1] : null;
|
|
343
|
+
const prevClose = prevPoint ? prevPoint.c : point.c;
|
|
344
|
+
const tr = Math.max(point.h - point.l, Math.abs(point.h - prevClose), Math.abs(point.l - prevClose));
|
|
345
|
+
trueRanges.push(tr);
|
|
346
|
+
}
|
|
347
|
+
let seed = 0;
|
|
348
|
+
let prevAtr = null;
|
|
349
|
+
for (let i = 0; i < trueRanges.length; i += 1) {
|
|
350
|
+
const tr = trueRanges[i];
|
|
351
|
+
if (tr === void 0) continue;
|
|
352
|
+
if (i < length) {
|
|
353
|
+
seed += tr;
|
|
354
|
+
if (i === length - 1) {
|
|
355
|
+
prevAtr = seed / length;
|
|
356
|
+
result[i] = prevAtr;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
prevAtr = prevAtr === null ? tr : (prevAtr * (length - 1) + tr) / length;
|
|
361
|
+
result[i] = prevAtr;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
365
|
+
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
366
|
+
if (!renderContext.yFromPrice) return;
|
|
367
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
368
|
+
ctx.save();
|
|
369
|
+
ctx.strokeStyle = color;
|
|
370
|
+
ctx.lineWidth = Math.max(1, width);
|
|
371
|
+
ctx.setLineDash([]);
|
|
372
|
+
let drawing = false;
|
|
373
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
374
|
+
const value = values[index];
|
|
375
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
376
|
+
drawing = false;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const x = renderContext.xFromIndex(index);
|
|
380
|
+
const y = yFromPrice(value);
|
|
381
|
+
if (!drawing) {
|
|
382
|
+
ctx.beginPath();
|
|
383
|
+
ctx.moveTo(x, y);
|
|
384
|
+
drawing = true;
|
|
385
|
+
} else {
|
|
386
|
+
ctx.lineTo(x, y);
|
|
387
|
+
}
|
|
388
|
+
const nextValue = values[index + 1];
|
|
389
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
390
|
+
ctx.stroke();
|
|
391
|
+
drawing = false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
ctx.restore();
|
|
395
|
+
};
|
|
396
|
+
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines) => {
|
|
397
|
+
const visible = [];
|
|
398
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
399
|
+
const value = values[index];
|
|
400
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
401
|
+
visible.push(value);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (visible.length === 0) return;
|
|
405
|
+
const minValue = minOverride ?? Math.min(...visible);
|
|
406
|
+
const maxValue = maxOverride ?? Math.max(...visible);
|
|
407
|
+
const range = maxValue - minValue || 1;
|
|
408
|
+
const yFromValue = (value) => {
|
|
409
|
+
const ratio = (value - minValue) / range;
|
|
410
|
+
return renderContext.chartBottom - ratio * renderContext.chartHeight;
|
|
411
|
+
};
|
|
412
|
+
if (guideLines && guideLines.length > 0) {
|
|
413
|
+
ctx.save();
|
|
414
|
+
ctx.strokeStyle = "rgba(148,163,184,0.35)";
|
|
415
|
+
ctx.lineWidth = 1;
|
|
416
|
+
ctx.setLineDash([4, 4]);
|
|
417
|
+
for (const guide of guideLines) {
|
|
418
|
+
const y = yFromValue(guide);
|
|
419
|
+
ctx.beginPath();
|
|
420
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
421
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
422
|
+
ctx.stroke();
|
|
423
|
+
}
|
|
424
|
+
ctx.restore();
|
|
425
|
+
}
|
|
426
|
+
ctx.save();
|
|
427
|
+
ctx.strokeStyle = color;
|
|
428
|
+
ctx.lineWidth = Math.max(1, width);
|
|
429
|
+
ctx.setLineDash([]);
|
|
430
|
+
let drawing = false;
|
|
431
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
432
|
+
const value = values[index];
|
|
433
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
434
|
+
drawing = false;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const x = renderContext.xFromIndex(index);
|
|
438
|
+
const y = yFromValue(value);
|
|
439
|
+
if (!drawing) {
|
|
440
|
+
ctx.beginPath();
|
|
441
|
+
ctx.moveTo(x, y);
|
|
442
|
+
drawing = true;
|
|
443
|
+
} else {
|
|
444
|
+
ctx.lineTo(x, y);
|
|
445
|
+
}
|
|
446
|
+
const nextValue = values[index + 1];
|
|
447
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
448
|
+
ctx.stroke();
|
|
449
|
+
drawing = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
ctx.restore();
|
|
453
|
+
};
|
|
454
|
+
var BUILTIN_VOLUME_INDICATOR = {
|
|
455
|
+
id: "volume",
|
|
456
|
+
name: "Volume",
|
|
457
|
+
pane: "separate",
|
|
458
|
+
paneHeightRatio: 0.22,
|
|
459
|
+
defaultInputs: {
|
|
460
|
+
upOpacity: 0.7,
|
|
461
|
+
downOpacity: 0.7,
|
|
462
|
+
minBarWidth: 1
|
|
463
|
+
},
|
|
464
|
+
draw: (ctx, renderContext, inputs) => {
|
|
465
|
+
const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
|
|
466
|
+
const paneHeight = Math.max(1, chartBottom - chartTop);
|
|
467
|
+
const visiblePoints = data.slice(startIndex, endIndex + 1);
|
|
468
|
+
const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
|
|
469
|
+
const barWidth = Math.max(
|
|
470
|
+
Math.max(1, Number(inputs.minBarWidth) || 1),
|
|
471
|
+
Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
|
|
472
|
+
);
|
|
473
|
+
const upOpacity = Math.min(1, Math.max(0.05, Number(inputs.upOpacity) || 0.7));
|
|
474
|
+
const downOpacity = Math.min(1, Math.max(0.05, Number(inputs.downOpacity) || 0.7));
|
|
475
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
476
|
+
const point = data[index];
|
|
477
|
+
if (!point || point.v === void 0 || point.v <= 0) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
|
|
481
|
+
const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
|
|
482
|
+
const xCenter = xFromIndex(index);
|
|
483
|
+
const barX = Math.round(xCenter - barWidth / 2);
|
|
484
|
+
const barY = Math.round(chartBottom - volumeHeight);
|
|
485
|
+
const isUp = point.c >= point.o;
|
|
486
|
+
const opacity = isUp ? upOpacity : downOpacity;
|
|
487
|
+
ctx.save();
|
|
488
|
+
ctx.globalAlpha = opacity;
|
|
489
|
+
ctx.fillStyle = isUp ? upColor : downColor;
|
|
490
|
+
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
491
|
+
ctx.restore();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
var BUILTIN_SMA_INDICATOR = {
|
|
496
|
+
id: "sma",
|
|
497
|
+
name: "SMA",
|
|
498
|
+
pane: "overlay",
|
|
499
|
+
defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
|
|
500
|
+
draw: (ctx, renderContext, inputs) => {
|
|
501
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
502
|
+
const values = computeSmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
503
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var BUILTIN_EMA_INDICATOR = {
|
|
507
|
+
id: "ema",
|
|
508
|
+
name: "EMA",
|
|
509
|
+
pane: "overlay",
|
|
510
|
+
defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
|
|
511
|
+
draw: (ctx, renderContext, inputs) => {
|
|
512
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
513
|
+
const values = computeEmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
514
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var BUILTIN_WMA_INDICATOR = {
|
|
518
|
+
id: "wma",
|
|
519
|
+
name: "WMA",
|
|
520
|
+
pane: "overlay",
|
|
521
|
+
defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
|
|
522
|
+
draw: (ctx, renderContext, inputs) => {
|
|
523
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
524
|
+
const values = computeWmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
525
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
var BUILTIN_VWMA_INDICATOR = {
|
|
529
|
+
id: "vwma",
|
|
530
|
+
name: "VWMA",
|
|
531
|
+
pane: "overlay",
|
|
532
|
+
defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
|
|
533
|
+
draw: (ctx, renderContext, inputs) => {
|
|
534
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
535
|
+
const values = computeVwmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
536
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var BUILTIN_RMA_INDICATOR = {
|
|
540
|
+
id: "rma",
|
|
541
|
+
name: "RMA",
|
|
542
|
+
pane: "overlay",
|
|
543
|
+
defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
|
|
544
|
+
draw: (ctx, renderContext, inputs) => {
|
|
545
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
546
|
+
const values = computeRmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
547
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
var BUILTIN_HMA_INDICATOR = {
|
|
551
|
+
id: "hma",
|
|
552
|
+
name: "HMA",
|
|
553
|
+
pane: "overlay",
|
|
554
|
+
defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
|
|
555
|
+
draw: (ctx, renderContext, inputs) => {
|
|
556
|
+
const length = clampIndicatorLength(inputs.length, 21);
|
|
557
|
+
const values = computeHmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
558
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
var BUILTIN_STDDEV_INDICATOR = {
|
|
562
|
+
id: "stddev",
|
|
563
|
+
name: "StdDev",
|
|
564
|
+
pane: "separate",
|
|
565
|
+
paneHeightRatio: 0.16,
|
|
566
|
+
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
567
|
+
draw: (ctx, renderContext, inputs) => {
|
|
568
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
569
|
+
const values = computeStdDevSeries(renderContext.data, length, inputs.source ?? "close");
|
|
570
|
+
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var BUILTIN_ATR_INDICATOR = {
|
|
574
|
+
id: "atr",
|
|
575
|
+
name: "ATR",
|
|
576
|
+
pane: "separate",
|
|
577
|
+
paneHeightRatio: 0.16,
|
|
578
|
+
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
579
|
+
draw: (ctx, renderContext, inputs) => {
|
|
580
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
581
|
+
const values = computeAtrSeries(renderContext.data, length);
|
|
582
|
+
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var BUILTIN_RSI_INDICATOR = {
|
|
586
|
+
id: "rsi",
|
|
587
|
+
name: "RSI",
|
|
588
|
+
pane: "separate",
|
|
589
|
+
paneHeightRatio: 0.18,
|
|
590
|
+
defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
|
|
591
|
+
draw: (ctx, renderContext, inputs) => {
|
|
592
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
593
|
+
const values = computeRsiSeries(renderContext.data, length);
|
|
594
|
+
drawSeparateSeries(
|
|
595
|
+
ctx,
|
|
596
|
+
renderContext,
|
|
597
|
+
values,
|
|
598
|
+
inputs.color ?? "#3b82f6",
|
|
599
|
+
Number(inputs.width) || 2,
|
|
600
|
+
0,
|
|
601
|
+
100,
|
|
602
|
+
[30, 50, 70]
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var BUILTIN_INDICATORS = [
|
|
607
|
+
BUILTIN_VOLUME_INDICATOR,
|
|
608
|
+
BUILTIN_SMA_INDICATOR,
|
|
609
|
+
BUILTIN_EMA_INDICATOR,
|
|
610
|
+
BUILTIN_RSI_INDICATOR,
|
|
611
|
+
BUILTIN_WMA_INDICATOR,
|
|
612
|
+
BUILTIN_VWMA_INDICATOR,
|
|
613
|
+
BUILTIN_RMA_INDICATOR,
|
|
614
|
+
BUILTIN_HMA_INDICATOR,
|
|
615
|
+
BUILTIN_STDDEV_INDICATOR,
|
|
616
|
+
BUILTIN_ATR_INDICATOR
|
|
617
|
+
];
|
|
150
618
|
function createChart(element, options = {}) {
|
|
151
619
|
const mergedOptions = {
|
|
152
620
|
...DEFAULT_OPTIONS,
|
|
@@ -197,6 +665,26 @@ function createChart(element, options = {}) {
|
|
|
197
665
|
let orderDragRegions = [];
|
|
198
666
|
let generatedPriceLineId = 1;
|
|
199
667
|
let generatedOrderLineId = 1;
|
|
668
|
+
let generatedIndicatorId = 1;
|
|
669
|
+
const indicatorRegistry = /* @__PURE__ */ new Map();
|
|
670
|
+
for (const indicator of BUILTIN_INDICATORS) {
|
|
671
|
+
indicatorRegistry.set(indicator.id, indicator);
|
|
672
|
+
}
|
|
673
|
+
const normalizeIndicatorState = (indicator) => {
|
|
674
|
+
const plugin = indicatorRegistry.get(indicator.type);
|
|
675
|
+
const defaults = plugin?.defaultInputs ?? {};
|
|
676
|
+
return {
|
|
677
|
+
id: indicator.id ?? `indicator-${generatedIndicatorId++}`,
|
|
678
|
+
type: indicator.type,
|
|
679
|
+
visible: indicator.visible ?? true,
|
|
680
|
+
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
681
|
+
inputs: {
|
|
682
|
+
...defaults,
|
|
683
|
+
...indicator.inputs ?? {}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
|
|
200
688
|
const orderWidgetWidthById = /* @__PURE__ */ new Map();
|
|
201
689
|
const orderPriceTagWidthById = /* @__PURE__ */ new Map();
|
|
202
690
|
let xCenter = 0;
|
|
@@ -209,6 +697,7 @@ function createChart(element, options = {}) {
|
|
|
209
697
|
let watermarkImage = null;
|
|
210
698
|
let watermarkImageReady = false;
|
|
211
699
|
let drawState = null;
|
|
700
|
+
let plotBottomForHit = 0;
|
|
212
701
|
let orderDragState = null;
|
|
213
702
|
let actionDragState = null;
|
|
214
703
|
let pointerDownInfo = null;
|
|
@@ -425,6 +914,39 @@ function createChart(element, options = {}) {
|
|
|
425
914
|
const extra = index - (data.length - 1);
|
|
426
915
|
return new Date(last.time.getTime() + extra * stepMs);
|
|
427
916
|
};
|
|
917
|
+
const findNearestIndexForTimeMs = (timeMs) => {
|
|
918
|
+
if (!Number.isFinite(timeMs) || data.length === 0) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
let low = 0;
|
|
922
|
+
let high = data.length - 1;
|
|
923
|
+
while (low <= high) {
|
|
924
|
+
const mid = Math.floor((low + high) / 2);
|
|
925
|
+
const midPoint = data[mid];
|
|
926
|
+
if (!midPoint) {
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
const midTime = midPoint.time.getTime();
|
|
930
|
+
if (midTime === timeMs) {
|
|
931
|
+
return mid;
|
|
932
|
+
}
|
|
933
|
+
if (midTime < timeMs) {
|
|
934
|
+
low = mid + 1;
|
|
935
|
+
} else {
|
|
936
|
+
high = mid - 1;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const lower = clamp(high, 0, data.length - 1);
|
|
940
|
+
const upper = clamp(low, 0, data.length - 1);
|
|
941
|
+
const lowerPoint = data[lower];
|
|
942
|
+
const upperPoint = data[upper];
|
|
943
|
+
if (!lowerPoint && !upperPoint) return null;
|
|
944
|
+
if (!lowerPoint) return upper;
|
|
945
|
+
if (!upperPoint) return lower;
|
|
946
|
+
const lowerDelta = Math.abs(lowerPoint.time.getTime() - timeMs);
|
|
947
|
+
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
948
|
+
return lowerDelta <= upperDelta ? lower : upper;
|
|
949
|
+
};
|
|
428
950
|
const formatHoverTimeLabel = (time, mode) => {
|
|
429
951
|
if (mode === "time") {
|
|
430
952
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -805,18 +1327,36 @@ function createChart(element, options = {}) {
|
|
|
805
1327
|
const chartLeft = margin.left;
|
|
806
1328
|
const chartTop = margin.top;
|
|
807
1329
|
const chartWidth = width - margin.left - margin.right;
|
|
808
|
-
const
|
|
809
|
-
const
|
|
1330
|
+
const fullChartHeight = height - margin.top - margin.bottom;
|
|
1331
|
+
const fullChartBottom = chartTop + fullChartHeight;
|
|
810
1332
|
const chartRight = chartLeft + chartWidth;
|
|
811
1333
|
const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
|
|
1334
|
+
const paneGap = 8;
|
|
1335
|
+
const separatePaneSpacing = 6;
|
|
1336
|
+
const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1337
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
|
|
1338
|
+
);
|
|
1339
|
+
const separatePaneHeightDefaults = activeSeparateIndicators.map(({ plugin }) => {
|
|
1340
|
+
const ratio = Math.min(0.45, Math.max(0.08, plugin.paneHeightRatio ?? 0.22));
|
|
1341
|
+
return Math.round(fullChartHeight * ratio);
|
|
1342
|
+
});
|
|
1343
|
+
const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
|
|
1344
|
+
const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
|
|
1345
|
+
const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
|
|
1346
|
+
const separatePaneHeights = separatePaneHeightDefaults.map((value) => Math.max(48, Math.round(value * separatePaneScale)));
|
|
1347
|
+
const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
|
|
1348
|
+
const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
|
|
1349
|
+
const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
|
|
1350
|
+
const chartBottom = chartTop + chartHeight;
|
|
812
1351
|
if (data.length === 0) {
|
|
1352
|
+
plotBottomForHit = fullChartBottom;
|
|
813
1353
|
drawState = {
|
|
814
1354
|
chartLeft,
|
|
815
1355
|
chartTop,
|
|
816
1356
|
chartRight,
|
|
817
|
-
chartBottom,
|
|
1357
|
+
chartBottom: fullChartBottom,
|
|
818
1358
|
chartWidth,
|
|
819
|
-
chartHeight,
|
|
1359
|
+
chartHeight: fullChartHeight,
|
|
820
1360
|
xStart: 0,
|
|
821
1361
|
xSpan: 0,
|
|
822
1362
|
yMin: 0,
|
|
@@ -884,6 +1424,7 @@ function createChart(element, options = {}) {
|
|
|
884
1424
|
yMin,
|
|
885
1425
|
yMax
|
|
886
1426
|
};
|
|
1427
|
+
plotBottomForHit = fullChartBottom;
|
|
887
1428
|
const yFromPrice = (price) => {
|
|
888
1429
|
return chartBottom - (price - yMin) / yRange * chartHeight;
|
|
889
1430
|
};
|
|
@@ -976,7 +1517,7 @@ function createChart(element, options = {}) {
|
|
|
976
1517
|
ctx.strokeStyle = gridColor;
|
|
977
1518
|
ctx.beginPath();
|
|
978
1519
|
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
979
|
-
ctx.lineTo(crisp(x), crisp(
|
|
1520
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
980
1521
|
ctx.stroke();
|
|
981
1522
|
}
|
|
982
1523
|
ctx.restore();
|
|
@@ -1004,6 +1545,36 @@ function createChart(element, options = {}) {
|
|
|
1004
1545
|
ctx.fillStyle = candleColor;
|
|
1005
1546
|
ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1006
1547
|
}
|
|
1548
|
+
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1549
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1550
|
+
);
|
|
1551
|
+
if (activeOverlayIndicators.length > 0) {
|
|
1552
|
+
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1553
|
+
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
1554
|
+
plugin.draw(
|
|
1555
|
+
ctx,
|
|
1556
|
+
{
|
|
1557
|
+
data,
|
|
1558
|
+
startIndex,
|
|
1559
|
+
endIndex,
|
|
1560
|
+
xStart,
|
|
1561
|
+
xSpan,
|
|
1562
|
+
chartLeft,
|
|
1563
|
+
chartRight,
|
|
1564
|
+
chartTop,
|
|
1565
|
+
chartBottom,
|
|
1566
|
+
chartWidth,
|
|
1567
|
+
chartHeight,
|
|
1568
|
+
xFromIndex,
|
|
1569
|
+
yFromPrice,
|
|
1570
|
+
candleSpacing,
|
|
1571
|
+
upColor: mergedOptions.upColor,
|
|
1572
|
+
downColor: mergedOptions.downColor
|
|
1573
|
+
},
|
|
1574
|
+
indicator.inputs
|
|
1575
|
+
);
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1007
1578
|
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1008
1579
|
if (crosshair.visible && crosshairPoint) {
|
|
1009
1580
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -1027,11 +1598,57 @@ function createChart(element, options = {}) {
|
|
|
1027
1598
|
ctx.restore();
|
|
1028
1599
|
}
|
|
1029
1600
|
ctx.restore();
|
|
1601
|
+
if (activeSeparateIndicators.length > 0) {
|
|
1602
|
+
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1603
|
+
let paneTopCursor = chartBottom + paneGap;
|
|
1604
|
+
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
1605
|
+
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
1606
|
+
const paneTop = paneTopCursor;
|
|
1607
|
+
const paneBottom = paneTop + paneHeight;
|
|
1608
|
+
paneTopCursor = paneBottom + separatePaneSpacing;
|
|
1609
|
+
ctx.save();
|
|
1610
|
+
ctx.beginPath();
|
|
1611
|
+
ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
|
|
1612
|
+
ctx.clip();
|
|
1613
|
+
plugin.draw(
|
|
1614
|
+
ctx,
|
|
1615
|
+
{
|
|
1616
|
+
data,
|
|
1617
|
+
startIndex,
|
|
1618
|
+
endIndex,
|
|
1619
|
+
xStart,
|
|
1620
|
+
xSpan,
|
|
1621
|
+
chartLeft,
|
|
1622
|
+
chartRight,
|
|
1623
|
+
chartTop: paneTop,
|
|
1624
|
+
chartBottom: paneBottom,
|
|
1625
|
+
chartWidth,
|
|
1626
|
+
chartHeight: paneHeight,
|
|
1627
|
+
xFromIndex,
|
|
1628
|
+
yFromPrice: null,
|
|
1629
|
+
candleSpacing,
|
|
1630
|
+
upColor: mergedOptions.upColor,
|
|
1631
|
+
downColor: mergedOptions.downColor
|
|
1632
|
+
},
|
|
1633
|
+
indicator.inputs
|
|
1634
|
+
);
|
|
1635
|
+
ctx.restore();
|
|
1636
|
+
ctx.save();
|
|
1637
|
+
ctx.strokeStyle = axis.lineColor;
|
|
1638
|
+
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
1639
|
+
ctx.beginPath();
|
|
1640
|
+
ctx.moveTo(crisp(chartLeft), crisp(paneTop));
|
|
1641
|
+
ctx.lineTo(crisp(chartRight), crisp(paneTop));
|
|
1642
|
+
ctx.stroke();
|
|
1643
|
+
ctx.restore();
|
|
1644
|
+
drawText(plugin.name.toUpperCase(), chartLeft + 6, paneTop + 12, "left", "middle", axis.textColor);
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1030
1647
|
ctx.strokeStyle = axis.lineColor;
|
|
1031
1648
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
1032
1649
|
ctx.beginPath();
|
|
1033
|
-
ctx.moveTo(crisp(chartLeft), crisp(
|
|
1034
|
-
ctx.lineTo(crisp(chartRight), crisp(
|
|
1650
|
+
ctx.moveTo(crisp(chartLeft), crisp(fullChartBottom));
|
|
1651
|
+
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
1035
1652
|
ctx.moveTo(crisp(chartRight), crisp(chartTop));
|
|
1036
1653
|
ctx.lineTo(crisp(chartRight), crisp(chartBottom));
|
|
1037
1654
|
ctx.stroke();
|
|
@@ -1095,7 +1712,7 @@ function createChart(element, options = {}) {
|
|
|
1095
1712
|
month: "short",
|
|
1096
1713
|
day: "numeric"
|
|
1097
1714
|
});
|
|
1098
|
-
drawText(timeLabel, x,
|
|
1715
|
+
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", axis.textColor);
|
|
1099
1716
|
}
|
|
1100
1717
|
if (crosshair.visible && crosshairPoint) {
|
|
1101
1718
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -1219,7 +1836,7 @@ function createChart(element, options = {}) {
|
|
|
1219
1836
|
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
1220
1837
|
const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
|
|
1221
1838
|
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
1222
|
-
const timeY =
|
|
1839
|
+
const timeY = fullChartBottom + 8;
|
|
1223
1840
|
ctx.fillStyle = labelBackground;
|
|
1224
1841
|
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, labelHeight, labelRadius);
|
|
1225
1842
|
strokeCrosshairLabel(timeX, timeY, timeWidth);
|
|
@@ -1426,11 +2043,12 @@ function createChart(element, options = {}) {
|
|
|
1426
2043
|
if (!drawState) {
|
|
1427
2044
|
return "outside";
|
|
1428
2045
|
}
|
|
1429
|
-
const
|
|
2046
|
+
const plotBottom = Math.max(drawState.chartBottom, plotBottomForHit);
|
|
2047
|
+
const inPlot = x >= drawState.chartLeft && x <= drawState.chartRight && y >= drawState.chartTop && y <= plotBottom;
|
|
1430
2048
|
if (inPlot) {
|
|
1431
2049
|
return "plot";
|
|
1432
2050
|
}
|
|
1433
|
-
const inXAxis = x >= drawState.chartLeft && x <= drawState.chartRight && y >
|
|
2051
|
+
const inXAxis = x >= drawState.chartLeft && x <= drawState.chartRight && y > plotBottom && y <= height;
|
|
1434
2052
|
if (inXAxis) {
|
|
1435
2053
|
return "x-axis";
|
|
1436
2054
|
}
|
|
@@ -1505,7 +2123,6 @@ function createChart(element, options = {}) {
|
|
|
1505
2123
|
lastPointerX = point.x;
|
|
1506
2124
|
lastPointerY = point.y;
|
|
1507
2125
|
canvas.setPointerCapture(event.pointerId);
|
|
1508
|
-
setCrosshairPoint(null);
|
|
1509
2126
|
};
|
|
1510
2127
|
const onPointerMove = (event) => {
|
|
1511
2128
|
const point = getCanvasPoint(event);
|
|
@@ -1587,7 +2204,7 @@ function createChart(element, options = {}) {
|
|
|
1587
2204
|
}
|
|
1588
2205
|
const hoverRegion = getHitRegion(point.x, point.y);
|
|
1589
2206
|
if (hoverRegion === "plot") {
|
|
1590
|
-
canvas.style.cursor = "default";
|
|
2207
|
+
canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
|
|
1591
2208
|
setCrosshairPoint(point);
|
|
1592
2209
|
emitCrosshairMove(point.x, point.y, "plot");
|
|
1593
2210
|
} else if (hoverRegion === "x-axis") {
|
|
@@ -1607,7 +2224,7 @@ function createChart(element, options = {}) {
|
|
|
1607
2224
|
const deltaX = point.x - lastPointerX;
|
|
1608
2225
|
const deltaY = point.y - lastPointerY;
|
|
1609
2226
|
if (dragMode === "plot") {
|
|
1610
|
-
canvas.style.cursor = "
|
|
2227
|
+
canvas.style.cursor = "grabbing";
|
|
1611
2228
|
pan(deltaX, deltaY, true, true);
|
|
1612
2229
|
setCrosshairPoint(null);
|
|
1613
2230
|
} else if (dragMode === "x-axis") {
|
|
@@ -1665,7 +2282,6 @@ function createChart(element, options = {}) {
|
|
|
1665
2282
|
dragMode = null;
|
|
1666
2283
|
activePointerId = null;
|
|
1667
2284
|
canvas.style.cursor = "default";
|
|
1668
|
-
setCrosshairPoint(null);
|
|
1669
2285
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
1670
2286
|
if (!pointerDownInfo.moved) {
|
|
1671
2287
|
const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
|
|
@@ -1675,10 +2291,21 @@ function createChart(element, options = {}) {
|
|
|
1675
2291
|
...clickPrice === void 0 ? {} : { price: clickPrice },
|
|
1676
2292
|
region: pointerDownInfo.region
|
|
1677
2293
|
});
|
|
2294
|
+
if (pointerDownInfo.region === "plot") {
|
|
2295
|
+
const clickPoint = { x: pointerDownInfo.x, y: pointerDownInfo.y };
|
|
2296
|
+
setCrosshairPoint(clickPoint);
|
|
2297
|
+
emitCrosshairMove(clickPoint.x, clickPoint.y, "plot");
|
|
2298
|
+
canvas.style.cursor = doubleClickEnabled ? "default" : "crosshair";
|
|
2299
|
+
} else {
|
|
2300
|
+
setCrosshairPoint(null);
|
|
2301
|
+
}
|
|
2302
|
+
} else {
|
|
2303
|
+
setCrosshairPoint(null);
|
|
1678
2304
|
}
|
|
1679
2305
|
pointerDownInfo = null;
|
|
1680
2306
|
} else if (!event) {
|
|
1681
2307
|
pointerDownInfo = null;
|
|
2308
|
+
setCrosshairPoint(null);
|
|
1682
2309
|
}
|
|
1683
2310
|
};
|
|
1684
2311
|
const onWheel = (event) => {
|
|
@@ -1746,6 +2373,9 @@ function createChart(element, options = {}) {
|
|
|
1746
2373
|
};
|
|
1747
2374
|
const setData = (nextData) => {
|
|
1748
2375
|
const hadData = data.length > 0;
|
|
2376
|
+
const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
|
|
2377
|
+
const previousCenterFraction = hadData ? xCenter - previousCenterRounded : 0;
|
|
2378
|
+
const previousCenterTimeMs = hadData && previousCenterRounded >= 0 && previousCenterRounded < data.length ? data[previousCenterRounded]?.time.getTime() ?? null : null;
|
|
1749
2379
|
data = parseData(nextData);
|
|
1750
2380
|
if (data.length === 0) {
|
|
1751
2381
|
xCenter = 0;
|
|
@@ -1759,6 +2389,12 @@ function createChart(element, options = {}) {
|
|
|
1759
2389
|
resetYViewport();
|
|
1760
2390
|
} else {
|
|
1761
2391
|
if (mergedOptions.preserveViewportOnDataUpdate) {
|
|
2392
|
+
if (previousCenterTimeMs !== null) {
|
|
2393
|
+
const nextCenter = findNearestIndexForTimeMs(previousCenterTimeMs);
|
|
2394
|
+
if (nextCenter !== null) {
|
|
2395
|
+
xCenter = nextCenter + previousCenterFraction;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
1762
2398
|
clampXViewport();
|
|
1763
2399
|
} else {
|
|
1764
2400
|
fitXViewport();
|
|
@@ -1835,6 +2471,59 @@ function createChart(element, options = {}) {
|
|
|
1835
2471
|
const setDoubleClickAction = (action) => {
|
|
1836
2472
|
doubleClickAction = action;
|
|
1837
2473
|
};
|
|
2474
|
+
const registerIndicator = (plugin) => {
|
|
2475
|
+
if (!plugin.id || typeof plugin.draw !== "function") {
|
|
2476
|
+
throw new Error("Invalid indicator plugin. Expected { id, draw }.");
|
|
2477
|
+
}
|
|
2478
|
+
indicatorRegistry.set(plugin.id, plugin);
|
|
2479
|
+
draw();
|
|
2480
|
+
};
|
|
2481
|
+
const unregisterIndicator = (type) => {
|
|
2482
|
+
indicatorRegistry.delete(type);
|
|
2483
|
+
indicators = indicators.filter((indicator) => indicator.type !== type);
|
|
2484
|
+
draw();
|
|
2485
|
+
};
|
|
2486
|
+
const setIndicators = (nextIndicators) => {
|
|
2487
|
+
indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
|
|
2488
|
+
draw();
|
|
2489
|
+
};
|
|
2490
|
+
const addIndicator = (type, inputs = {}, options2 = {}) => {
|
|
2491
|
+
const plugin = indicatorRegistry.get(type);
|
|
2492
|
+
if (!plugin) {
|
|
2493
|
+
throw new Error(`Unknown indicator type "${type}". Register it first.`);
|
|
2494
|
+
}
|
|
2495
|
+
const next = normalizeIndicatorState({
|
|
2496
|
+
...options2,
|
|
2497
|
+
type,
|
|
2498
|
+
inputs
|
|
2499
|
+
});
|
|
2500
|
+
indicators.push(next);
|
|
2501
|
+
draw();
|
|
2502
|
+
return next.id;
|
|
2503
|
+
};
|
|
2504
|
+
const updateIndicator = (id, patch) => {
|
|
2505
|
+
indicators = indicators.map((indicator) => {
|
|
2506
|
+
if (indicator.id !== id) {
|
|
2507
|
+
return indicator;
|
|
2508
|
+
}
|
|
2509
|
+
const plugin = indicatorRegistry.get(indicator.type);
|
|
2510
|
+
return {
|
|
2511
|
+
...indicator,
|
|
2512
|
+
visible: patch.visible ?? indicator.visible,
|
|
2513
|
+
pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
|
|
2514
|
+
type: patch.type ?? indicator.type,
|
|
2515
|
+
inputs: {
|
|
2516
|
+
...indicator.inputs,
|
|
2517
|
+
...patch.inputs ?? {}
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
});
|
|
2521
|
+
draw();
|
|
2522
|
+
};
|
|
2523
|
+
const removeIndicator = (id) => {
|
|
2524
|
+
indicators = indicators.filter((indicator) => indicator.id !== id);
|
|
2525
|
+
draw();
|
|
2526
|
+
};
|
|
1838
2527
|
const destroy = () => {
|
|
1839
2528
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
1840
2529
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
@@ -1869,6 +2558,12 @@ function createChart(element, options = {}) {
|
|
|
1869
2558
|
fitContent,
|
|
1870
2559
|
setDoubleClickEnabled,
|
|
1871
2560
|
setDoubleClickAction,
|
|
2561
|
+
registerIndicator,
|
|
2562
|
+
unregisterIndicator,
|
|
2563
|
+
addIndicator,
|
|
2564
|
+
updateIndicator,
|
|
2565
|
+
removeIndicator,
|
|
2566
|
+
setIndicators,
|
|
1872
2567
|
resize,
|
|
1873
2568
|
destroy
|
|
1874
2569
|
};
|