hyperprop-charting-library 0.1.20 → 0.1.22
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/README.md +50 -0
- package/dist/hyperprop-charting-library.cjs +704 -12
- package/dist/hyperprop-charting-library.d.ts +50 -1
- package/dist/hyperprop-charting-library.js +704 -12
- package/dist/index.cjs +704 -12
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +704 -12
- package/docs/API.md +63 -0
- package/docs/RECIPES.md +61 -0
- package/package.json +1 -1
|
@@ -169,8 +169,481 @@ var DEFAULT_OPTIONS = {
|
|
|
169
169
|
labelTextColor: "#0b1220",
|
|
170
170
|
labelBorderRadius: 3
|
|
171
171
|
},
|
|
172
|
-
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
172
|
+
dashPatterns: DEFAULT_DASH_PATTERNS,
|
|
173
|
+
indicators: []
|
|
173
174
|
};
|
|
175
|
+
var getIndicatorSourceValue = (point, source) => {
|
|
176
|
+
if (source === "open") return point.o;
|
|
177
|
+
if (source === "high") return point.h;
|
|
178
|
+
if (source === "low") return point.l;
|
|
179
|
+
if (source === "hl2") return (point.h + point.l) / 2;
|
|
180
|
+
return point.c;
|
|
181
|
+
};
|
|
182
|
+
var clampIndicatorLength = (value, fallback) => {
|
|
183
|
+
return Math.max(1, Math.round(Number(value) || fallback));
|
|
184
|
+
};
|
|
185
|
+
var computeSmaSeries = (data, length, source) => {
|
|
186
|
+
const result = new Array(data.length).fill(null);
|
|
187
|
+
let sum = 0;
|
|
188
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
189
|
+
const point = data[i];
|
|
190
|
+
if (!point) continue;
|
|
191
|
+
const value = getIndicatorSourceValue(point, source);
|
|
192
|
+
sum += value;
|
|
193
|
+
if (i >= length) {
|
|
194
|
+
const oldPoint = data[i - length];
|
|
195
|
+
if (oldPoint) {
|
|
196
|
+
sum -= getIndicatorSourceValue(oldPoint, source);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (i >= length - 1) {
|
|
200
|
+
result[i] = sum / length;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
};
|
|
205
|
+
var computeEmaSeries = (data, length, source) => {
|
|
206
|
+
const result = new Array(data.length).fill(null);
|
|
207
|
+
const alpha = 2 / (length + 1);
|
|
208
|
+
let prev = null;
|
|
209
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
210
|
+
const point = data[i];
|
|
211
|
+
if (!point) continue;
|
|
212
|
+
const value = getIndicatorSourceValue(point, source);
|
|
213
|
+
prev = prev === null ? value : alpha * value + (1 - alpha) * prev;
|
|
214
|
+
if (i >= length - 1) {
|
|
215
|
+
result[i] = prev;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
};
|
|
220
|
+
var computeRmaSeries = (data, length, source) => {
|
|
221
|
+
const result = new Array(data.length).fill(null);
|
|
222
|
+
let prev = null;
|
|
223
|
+
let seedSum = 0;
|
|
224
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
225
|
+
const point = data[i];
|
|
226
|
+
if (!point) continue;
|
|
227
|
+
const value = getIndicatorSourceValue(point, source);
|
|
228
|
+
if (i < length) {
|
|
229
|
+
seedSum += value;
|
|
230
|
+
if (i === length - 1) {
|
|
231
|
+
prev = seedSum / length;
|
|
232
|
+
result[i] = prev;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
prev = prev === null ? value : (prev * (length - 1) + value) / length;
|
|
237
|
+
result[i] = prev;
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
};
|
|
241
|
+
var computeWmaSeriesFromValues = (values, length) => {
|
|
242
|
+
const result = new Array(values.length).fill(null);
|
|
243
|
+
const denominator = length * (length + 1) / 2;
|
|
244
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
245
|
+
if (i < length - 1) continue;
|
|
246
|
+
let weighted = 0;
|
|
247
|
+
let valid = true;
|
|
248
|
+
for (let j = 0; j < length; j += 1) {
|
|
249
|
+
const value = values[i - length + 1 + j];
|
|
250
|
+
if (value == null) {
|
|
251
|
+
valid = false;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
weighted += value * (j + 1);
|
|
255
|
+
}
|
|
256
|
+
if (valid) {
|
|
257
|
+
result[i] = weighted / denominator;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
};
|
|
262
|
+
var computeWmaSeries = (data, length, source) => {
|
|
263
|
+
const sourceValues = data.map((point) => getIndicatorSourceValue(point, source));
|
|
264
|
+
return computeWmaSeriesFromValues(sourceValues, length);
|
|
265
|
+
};
|
|
266
|
+
var computeVwmaSeries = (data, length, source) => {
|
|
267
|
+
const result = new Array(data.length).fill(null);
|
|
268
|
+
let sumPv = 0;
|
|
269
|
+
let sumV = 0;
|
|
270
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
271
|
+
const point = data[i];
|
|
272
|
+
if (!point) continue;
|
|
273
|
+
const sourceValue = getIndicatorSourceValue(point, source);
|
|
274
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
275
|
+
sumPv += sourceValue * volume;
|
|
276
|
+
sumV += volume;
|
|
277
|
+
if (i >= length) {
|
|
278
|
+
const oldPoint = data[i - length];
|
|
279
|
+
if (oldPoint) {
|
|
280
|
+
const oldSource = getIndicatorSourceValue(oldPoint, source);
|
|
281
|
+
const oldVolume = Math.max(0, oldPoint.v ?? 0);
|
|
282
|
+
sumPv -= oldSource * oldVolume;
|
|
283
|
+
sumV -= oldVolume;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (i >= length - 1 && sumV > 0) {
|
|
287
|
+
result[i] = sumPv / sumV;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
};
|
|
292
|
+
var computeHmaSeries = (data, length, source) => {
|
|
293
|
+
const halfLength = Math.max(1, Math.floor(length / 2));
|
|
294
|
+
const sqrtLength = Math.max(1, Math.round(Math.sqrt(length)));
|
|
295
|
+
const wmaHalf = computeWmaSeries(data, halfLength, source);
|
|
296
|
+
const wmaFull = computeWmaSeries(data, length, source);
|
|
297
|
+
const diff = wmaHalf.map((value, idx) => {
|
|
298
|
+
const full = wmaFull[idx];
|
|
299
|
+
if (value == null || full == null) return null;
|
|
300
|
+
return 2 * value - full;
|
|
301
|
+
});
|
|
302
|
+
return computeWmaSeriesFromValues(diff, sqrtLength);
|
|
303
|
+
};
|
|
304
|
+
var computeStdDevSeries = (data, length, source) => {
|
|
305
|
+
const result = new Array(data.length).fill(null);
|
|
306
|
+
const values = data.map((point) => point ? getIndicatorSourceValue(point, source) : 0);
|
|
307
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
308
|
+
if (i < length - 1) continue;
|
|
309
|
+
let sum = 0;
|
|
310
|
+
for (let j = 0; j < length; j += 1) {
|
|
311
|
+
sum += values[i - j] ?? 0;
|
|
312
|
+
}
|
|
313
|
+
const mean = sum / length;
|
|
314
|
+
let variance = 0;
|
|
315
|
+
for (let j = 0; j < length; j += 1) {
|
|
316
|
+
const delta = (values[i - j] ?? 0) - mean;
|
|
317
|
+
variance += delta * delta;
|
|
318
|
+
}
|
|
319
|
+
result[i] = Math.sqrt(variance / length);
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
};
|
|
323
|
+
var computeRsiSeries = (data, length) => {
|
|
324
|
+
const result = new Array(data.length).fill(null);
|
|
325
|
+
if (data.length < 2) return result;
|
|
326
|
+
let avgGain = 0;
|
|
327
|
+
let avgLoss = 0;
|
|
328
|
+
let seeded = false;
|
|
329
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
330
|
+
const point = data[i];
|
|
331
|
+
const prevPoint = data[i - 1];
|
|
332
|
+
if (!point || !prevPoint) continue;
|
|
333
|
+
const delta = point.c - prevPoint.c;
|
|
334
|
+
const gain = Math.max(0, delta);
|
|
335
|
+
const loss = Math.max(0, -delta);
|
|
336
|
+
if (i <= length) {
|
|
337
|
+
avgGain += gain;
|
|
338
|
+
avgLoss += loss;
|
|
339
|
+
if (i === length) {
|
|
340
|
+
avgGain /= length;
|
|
341
|
+
avgLoss /= length;
|
|
342
|
+
seeded = true;
|
|
343
|
+
}
|
|
344
|
+
} else if (seeded) {
|
|
345
|
+
avgGain = (avgGain * (length - 1) + gain) / length;
|
|
346
|
+
avgLoss = (avgLoss * (length - 1) + loss) / length;
|
|
347
|
+
}
|
|
348
|
+
if (seeded && i >= length) {
|
|
349
|
+
if (avgLoss === 0) {
|
|
350
|
+
result[i] = 100;
|
|
351
|
+
} else {
|
|
352
|
+
const rs = avgGain / avgLoss;
|
|
353
|
+
result[i] = 100 - 100 / (1 + rs);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
};
|
|
359
|
+
var computeAtrSeries = (data, length) => {
|
|
360
|
+
const result = new Array(data.length).fill(null);
|
|
361
|
+
if (data.length === 0) return result;
|
|
362
|
+
const trueRanges = [];
|
|
363
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
364
|
+
const point = data[i];
|
|
365
|
+
if (!point) continue;
|
|
366
|
+
const prevPoint = i > 0 ? data[i - 1] : null;
|
|
367
|
+
const prevClose = prevPoint ? prevPoint.c : point.c;
|
|
368
|
+
const tr = Math.max(point.h - point.l, Math.abs(point.h - prevClose), Math.abs(point.l - prevClose));
|
|
369
|
+
trueRanges.push(tr);
|
|
370
|
+
}
|
|
371
|
+
let seed = 0;
|
|
372
|
+
let prevAtr = null;
|
|
373
|
+
for (let i = 0; i < trueRanges.length; i += 1) {
|
|
374
|
+
const tr = trueRanges[i];
|
|
375
|
+
if (tr === void 0) continue;
|
|
376
|
+
if (i < length) {
|
|
377
|
+
seed += tr;
|
|
378
|
+
if (i === length - 1) {
|
|
379
|
+
prevAtr = seed / length;
|
|
380
|
+
result[i] = prevAtr;
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
prevAtr = prevAtr === null ? tr : (prevAtr * (length - 1) + tr) / length;
|
|
385
|
+
result[i] = prevAtr;
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
};
|
|
389
|
+
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
390
|
+
if (!renderContext.yFromPrice) return;
|
|
391
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
392
|
+
ctx.save();
|
|
393
|
+
ctx.strokeStyle = color;
|
|
394
|
+
ctx.lineWidth = Math.max(1, width);
|
|
395
|
+
ctx.setLineDash([]);
|
|
396
|
+
let drawing = false;
|
|
397
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
398
|
+
const value = values[index];
|
|
399
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
400
|
+
drawing = false;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const x = renderContext.xFromIndex(index);
|
|
404
|
+
const y = yFromPrice(value);
|
|
405
|
+
if (!drawing) {
|
|
406
|
+
ctx.beginPath();
|
|
407
|
+
ctx.moveTo(x, y);
|
|
408
|
+
drawing = true;
|
|
409
|
+
} else {
|
|
410
|
+
ctx.lineTo(x, y);
|
|
411
|
+
}
|
|
412
|
+
const nextValue = values[index + 1];
|
|
413
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
414
|
+
ctx.stroke();
|
|
415
|
+
drawing = false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
ctx.restore();
|
|
419
|
+
};
|
|
420
|
+
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines) => {
|
|
421
|
+
const visible = [];
|
|
422
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
423
|
+
const value = values[index];
|
|
424
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
425
|
+
visible.push(value);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (visible.length === 0) return;
|
|
429
|
+
const minValue = minOverride ?? Math.min(...visible);
|
|
430
|
+
const maxValue = maxOverride ?? Math.max(...visible);
|
|
431
|
+
const range = maxValue - minValue || 1;
|
|
432
|
+
const yFromValue = (value) => {
|
|
433
|
+
const ratio = (value - minValue) / range;
|
|
434
|
+
return renderContext.chartBottom - ratio * renderContext.chartHeight;
|
|
435
|
+
};
|
|
436
|
+
if (guideLines && guideLines.length > 0) {
|
|
437
|
+
ctx.save();
|
|
438
|
+
ctx.strokeStyle = "rgba(148,163,184,0.35)";
|
|
439
|
+
ctx.lineWidth = 1;
|
|
440
|
+
ctx.setLineDash([4, 4]);
|
|
441
|
+
for (const guide of guideLines) {
|
|
442
|
+
const y = yFromValue(guide);
|
|
443
|
+
ctx.beginPath();
|
|
444
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
445
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
446
|
+
ctx.stroke();
|
|
447
|
+
}
|
|
448
|
+
ctx.restore();
|
|
449
|
+
}
|
|
450
|
+
ctx.save();
|
|
451
|
+
ctx.strokeStyle = color;
|
|
452
|
+
ctx.lineWidth = Math.max(1, width);
|
|
453
|
+
ctx.setLineDash([]);
|
|
454
|
+
let drawing = false;
|
|
455
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
456
|
+
const value = values[index];
|
|
457
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
458
|
+
drawing = false;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const x = renderContext.xFromIndex(index);
|
|
462
|
+
const y = yFromValue(value);
|
|
463
|
+
if (!drawing) {
|
|
464
|
+
ctx.beginPath();
|
|
465
|
+
ctx.moveTo(x, y);
|
|
466
|
+
drawing = true;
|
|
467
|
+
} else {
|
|
468
|
+
ctx.lineTo(x, y);
|
|
469
|
+
}
|
|
470
|
+
const nextValue = values[index + 1];
|
|
471
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
472
|
+
ctx.stroke();
|
|
473
|
+
drawing = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
ctx.restore();
|
|
477
|
+
};
|
|
478
|
+
var BUILTIN_VOLUME_INDICATOR = {
|
|
479
|
+
id: "volume",
|
|
480
|
+
name: "Volume",
|
|
481
|
+
pane: "overlay",
|
|
482
|
+
paneHeightRatio: 0.22,
|
|
483
|
+
defaultInputs: {
|
|
484
|
+
upOpacity: 0.7,
|
|
485
|
+
downOpacity: 0.7,
|
|
486
|
+
minBarWidth: 1,
|
|
487
|
+
overlayHeightRatio: 0.22
|
|
488
|
+
},
|
|
489
|
+
draw: (ctx, renderContext, inputs) => {
|
|
490
|
+
const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
|
|
491
|
+
const fullPaneHeight = Math.max(1, chartBottom - chartTop);
|
|
492
|
+
const overlayMode = renderContext.yFromPrice !== null;
|
|
493
|
+
const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
|
|
494
|
+
const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
|
|
495
|
+
const paneBottom = chartBottom;
|
|
496
|
+
const visiblePoints = data.slice(startIndex, endIndex + 1);
|
|
497
|
+
const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
|
|
498
|
+
const barWidth = Math.max(
|
|
499
|
+
Math.max(1, Number(inputs.minBarWidth) || 1),
|
|
500
|
+
Math.min(Math.max(1, candleSpacing - 1), Math.floor(candleSpacing * 0.7))
|
|
501
|
+
);
|
|
502
|
+
const upOpacity = Math.min(1, Math.max(0.05, Number(inputs.upOpacity) || 0.7));
|
|
503
|
+
const downOpacity = Math.min(1, Math.max(0.05, Number(inputs.downOpacity) || 0.7));
|
|
504
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
505
|
+
const point = data[index];
|
|
506
|
+
if (!point || point.v === void 0 || point.v <= 0) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const ratio = Math.min(1, Math.max(0, point.v / maxVolume));
|
|
510
|
+
const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
|
|
511
|
+
const xCenter = xFromIndex(index);
|
|
512
|
+
const barX = Math.round(xCenter - barWidth / 2);
|
|
513
|
+
const barY = Math.round(paneBottom - volumeHeight);
|
|
514
|
+
const isUp = point.c >= point.o;
|
|
515
|
+
const opacity = isUp ? upOpacity : downOpacity;
|
|
516
|
+
ctx.save();
|
|
517
|
+
ctx.globalAlpha = opacity;
|
|
518
|
+
ctx.fillStyle = isUp ? upColor : downColor;
|
|
519
|
+
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
520
|
+
ctx.restore();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var BUILTIN_SMA_INDICATOR = {
|
|
525
|
+
id: "sma",
|
|
526
|
+
name: "SMA",
|
|
527
|
+
pane: "overlay",
|
|
528
|
+
defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
|
|
529
|
+
draw: (ctx, renderContext, inputs) => {
|
|
530
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
531
|
+
const values = computeSmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
532
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
var BUILTIN_EMA_INDICATOR = {
|
|
536
|
+
id: "ema",
|
|
537
|
+
name: "EMA",
|
|
538
|
+
pane: "overlay",
|
|
539
|
+
defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
|
|
540
|
+
draw: (ctx, renderContext, inputs) => {
|
|
541
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
542
|
+
const values = computeEmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
543
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var BUILTIN_WMA_INDICATOR = {
|
|
547
|
+
id: "wma",
|
|
548
|
+
name: "WMA",
|
|
549
|
+
pane: "overlay",
|
|
550
|
+
defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
|
|
551
|
+
draw: (ctx, renderContext, inputs) => {
|
|
552
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
553
|
+
const values = computeWmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
554
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var BUILTIN_VWMA_INDICATOR = {
|
|
558
|
+
id: "vwma",
|
|
559
|
+
name: "VWMA",
|
|
560
|
+
pane: "overlay",
|
|
561
|
+
defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
|
|
562
|
+
draw: (ctx, renderContext, inputs) => {
|
|
563
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
564
|
+
const values = computeVwmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
565
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var BUILTIN_RMA_INDICATOR = {
|
|
569
|
+
id: "rma",
|
|
570
|
+
name: "RMA",
|
|
571
|
+
pane: "overlay",
|
|
572
|
+
defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
|
|
573
|
+
draw: (ctx, renderContext, inputs) => {
|
|
574
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
575
|
+
const values = computeRmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
576
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var BUILTIN_HMA_INDICATOR = {
|
|
580
|
+
id: "hma",
|
|
581
|
+
name: "HMA",
|
|
582
|
+
pane: "overlay",
|
|
583
|
+
defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
|
|
584
|
+
draw: (ctx, renderContext, inputs) => {
|
|
585
|
+
const length = clampIndicatorLength(inputs.length, 21);
|
|
586
|
+
const values = computeHmaSeries(renderContext.data, length, inputs.source ?? "close");
|
|
587
|
+
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
var BUILTIN_STDDEV_INDICATOR = {
|
|
591
|
+
id: "stddev",
|
|
592
|
+
name: "StdDev",
|
|
593
|
+
pane: "separate",
|
|
594
|
+
paneHeightRatio: 0.16,
|
|
595
|
+
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
596
|
+
draw: (ctx, renderContext, inputs) => {
|
|
597
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
598
|
+
const values = computeStdDevSeries(renderContext.data, length, inputs.source ?? "close");
|
|
599
|
+
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
var BUILTIN_ATR_INDICATOR = {
|
|
603
|
+
id: "atr",
|
|
604
|
+
name: "ATR",
|
|
605
|
+
pane: "separate",
|
|
606
|
+
paneHeightRatio: 0.16,
|
|
607
|
+
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
608
|
+
draw: (ctx, renderContext, inputs) => {
|
|
609
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
610
|
+
const values = computeAtrSeries(renderContext.data, length);
|
|
611
|
+
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
var BUILTIN_RSI_INDICATOR = {
|
|
615
|
+
id: "rsi",
|
|
616
|
+
name: "RSI",
|
|
617
|
+
pane: "separate",
|
|
618
|
+
paneHeightRatio: 0.18,
|
|
619
|
+
defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
|
|
620
|
+
draw: (ctx, renderContext, inputs) => {
|
|
621
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
622
|
+
const values = computeRsiSeries(renderContext.data, length);
|
|
623
|
+
drawSeparateSeries(
|
|
624
|
+
ctx,
|
|
625
|
+
renderContext,
|
|
626
|
+
values,
|
|
627
|
+
inputs.color ?? "#3b82f6",
|
|
628
|
+
Number(inputs.width) || 2,
|
|
629
|
+
0,
|
|
630
|
+
100,
|
|
631
|
+
[30, 50, 70]
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
var BUILTIN_INDICATORS = [
|
|
636
|
+
BUILTIN_VOLUME_INDICATOR,
|
|
637
|
+
BUILTIN_SMA_INDICATOR,
|
|
638
|
+
BUILTIN_EMA_INDICATOR,
|
|
639
|
+
BUILTIN_RSI_INDICATOR,
|
|
640
|
+
BUILTIN_WMA_INDICATOR,
|
|
641
|
+
BUILTIN_VWMA_INDICATOR,
|
|
642
|
+
BUILTIN_RMA_INDICATOR,
|
|
643
|
+
BUILTIN_HMA_INDICATOR,
|
|
644
|
+
BUILTIN_STDDEV_INDICATOR,
|
|
645
|
+
BUILTIN_ATR_INDICATOR
|
|
646
|
+
];
|
|
174
647
|
function createChart(element, options = {}) {
|
|
175
648
|
const mergedOptions = {
|
|
176
649
|
...DEFAULT_OPTIONS,
|
|
@@ -221,6 +694,27 @@ function createChart(element, options = {}) {
|
|
|
221
694
|
let orderDragRegions = [];
|
|
222
695
|
let generatedPriceLineId = 1;
|
|
223
696
|
let generatedOrderLineId = 1;
|
|
697
|
+
let generatedIndicatorId = 1;
|
|
698
|
+
const indicatorRegistry = /* @__PURE__ */ new Map();
|
|
699
|
+
for (const indicator of BUILTIN_INDICATORS) {
|
|
700
|
+
indicatorRegistry.set(indicator.id, indicator);
|
|
701
|
+
}
|
|
702
|
+
const normalizeIndicatorState = (indicator) => {
|
|
703
|
+
const plugin = indicatorRegistry.get(indicator.type);
|
|
704
|
+
const defaults = plugin?.defaultInputs ?? {};
|
|
705
|
+
return {
|
|
706
|
+
id: indicator.id ?? `indicator-${generatedIndicatorId++}`,
|
|
707
|
+
type: indicator.type,
|
|
708
|
+
visible: indicator.visible ?? true,
|
|
709
|
+
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
710
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
711
|
+
inputs: {
|
|
712
|
+
...defaults,
|
|
713
|
+
...indicator.inputs ?? {}
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
let indicators = (options.indicators ?? []).map((indicator) => normalizeIndicatorState(indicator));
|
|
224
718
|
const orderWidgetWidthById = /* @__PURE__ */ new Map();
|
|
225
719
|
const orderPriceTagWidthById = /* @__PURE__ */ new Map();
|
|
226
720
|
let xCenter = 0;
|
|
@@ -233,6 +727,7 @@ function createChart(element, options = {}) {
|
|
|
233
727
|
let watermarkImage = null;
|
|
234
728
|
let watermarkImageReady = false;
|
|
235
729
|
let drawState = null;
|
|
730
|
+
let plotBottomForHit = 0;
|
|
236
731
|
let orderDragState = null;
|
|
237
732
|
let actionDragState = null;
|
|
238
733
|
let pointerDownInfo = null;
|
|
@@ -449,6 +944,39 @@ function createChart(element, options = {}) {
|
|
|
449
944
|
const extra = index - (data.length - 1);
|
|
450
945
|
return new Date(last.time.getTime() + extra * stepMs);
|
|
451
946
|
};
|
|
947
|
+
const findNearestIndexForTimeMs = (timeMs) => {
|
|
948
|
+
if (!Number.isFinite(timeMs) || data.length === 0) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
let low = 0;
|
|
952
|
+
let high = data.length - 1;
|
|
953
|
+
while (low <= high) {
|
|
954
|
+
const mid = Math.floor((low + high) / 2);
|
|
955
|
+
const midPoint = data[mid];
|
|
956
|
+
if (!midPoint) {
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
const midTime = midPoint.time.getTime();
|
|
960
|
+
if (midTime === timeMs) {
|
|
961
|
+
return mid;
|
|
962
|
+
}
|
|
963
|
+
if (midTime < timeMs) {
|
|
964
|
+
low = mid + 1;
|
|
965
|
+
} else {
|
|
966
|
+
high = mid - 1;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const lower = clamp(high, 0, data.length - 1);
|
|
970
|
+
const upper = clamp(low, 0, data.length - 1);
|
|
971
|
+
const lowerPoint = data[lower];
|
|
972
|
+
const upperPoint = data[upper];
|
|
973
|
+
if (!lowerPoint && !upperPoint) return null;
|
|
974
|
+
if (!lowerPoint) return upper;
|
|
975
|
+
if (!upperPoint) return lower;
|
|
976
|
+
const lowerDelta = Math.abs(lowerPoint.time.getTime() - timeMs);
|
|
977
|
+
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
978
|
+
return lowerDelta <= upperDelta ? lower : upper;
|
|
979
|
+
};
|
|
452
980
|
const formatHoverTimeLabel = (time, mode) => {
|
|
453
981
|
if (mode === "time") {
|
|
454
982
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -829,18 +1357,36 @@ function createChart(element, options = {}) {
|
|
|
829
1357
|
const chartLeft = margin.left;
|
|
830
1358
|
const chartTop = margin.top;
|
|
831
1359
|
const chartWidth = width - margin.left - margin.right;
|
|
832
|
-
const
|
|
833
|
-
const
|
|
1360
|
+
const fullChartHeight = height - margin.top - margin.bottom;
|
|
1361
|
+
const fullChartBottom = chartTop + fullChartHeight;
|
|
834
1362
|
const chartRight = chartLeft + chartWidth;
|
|
835
1363
|
const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
|
|
1364
|
+
const paneGap = 8;
|
|
1365
|
+
const separatePaneSpacing = 6;
|
|
1366
|
+
const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1367
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
|
|
1368
|
+
);
|
|
1369
|
+
const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
|
|
1370
|
+
const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
|
|
1371
|
+
return Math.round(fullChartHeight * ratio);
|
|
1372
|
+
});
|
|
1373
|
+
const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
|
|
1374
|
+
const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
|
|
1375
|
+
const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
|
|
1376
|
+
const separatePaneHeights = separatePaneHeightDefaults.map((value) => Math.max(48, Math.round(value * separatePaneScale)));
|
|
1377
|
+
const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
|
|
1378
|
+
const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
|
|
1379
|
+
const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
|
|
1380
|
+
const chartBottom = chartTop + chartHeight;
|
|
836
1381
|
if (data.length === 0) {
|
|
1382
|
+
plotBottomForHit = fullChartBottom;
|
|
837
1383
|
drawState = {
|
|
838
1384
|
chartLeft,
|
|
839
1385
|
chartTop,
|
|
840
1386
|
chartRight,
|
|
841
|
-
chartBottom,
|
|
1387
|
+
chartBottom: fullChartBottom,
|
|
842
1388
|
chartWidth,
|
|
843
|
-
chartHeight,
|
|
1389
|
+
chartHeight: fullChartHeight,
|
|
844
1390
|
xStart: 0,
|
|
845
1391
|
xSpan: 0,
|
|
846
1392
|
yMin: 0,
|
|
@@ -908,6 +1454,7 @@ function createChart(element, options = {}) {
|
|
|
908
1454
|
yMin,
|
|
909
1455
|
yMax
|
|
910
1456
|
};
|
|
1457
|
+
plotBottomForHit = fullChartBottom;
|
|
911
1458
|
const yFromPrice = (price) => {
|
|
912
1459
|
return chartBottom - (price - yMin) / yRange * chartHeight;
|
|
913
1460
|
};
|
|
@@ -1000,7 +1547,7 @@ function createChart(element, options = {}) {
|
|
|
1000
1547
|
ctx.strokeStyle = gridColor;
|
|
1001
1548
|
ctx.beginPath();
|
|
1002
1549
|
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
1003
|
-
ctx.lineTo(crisp(x), crisp(
|
|
1550
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
1004
1551
|
ctx.stroke();
|
|
1005
1552
|
}
|
|
1006
1553
|
ctx.restore();
|
|
@@ -1028,6 +1575,36 @@ function createChart(element, options = {}) {
|
|
|
1028
1575
|
ctx.fillStyle = candleColor;
|
|
1029
1576
|
ctx.fillRect(Math.round(centerX - bodyWidth / 2), Math.round(bodyTop), bodyWidth, Math.max(1, Math.round(bodyHeight)));
|
|
1030
1577
|
}
|
|
1578
|
+
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1579
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1580
|
+
);
|
|
1581
|
+
if (activeOverlayIndicators.length > 0) {
|
|
1582
|
+
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1583
|
+
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
1584
|
+
plugin.draw(
|
|
1585
|
+
ctx,
|
|
1586
|
+
{
|
|
1587
|
+
data,
|
|
1588
|
+
startIndex,
|
|
1589
|
+
endIndex,
|
|
1590
|
+
xStart,
|
|
1591
|
+
xSpan,
|
|
1592
|
+
chartLeft,
|
|
1593
|
+
chartRight,
|
|
1594
|
+
chartTop,
|
|
1595
|
+
chartBottom,
|
|
1596
|
+
chartWidth,
|
|
1597
|
+
chartHeight,
|
|
1598
|
+
xFromIndex,
|
|
1599
|
+
yFromPrice,
|
|
1600
|
+
candleSpacing,
|
|
1601
|
+
upColor: mergedOptions.upColor,
|
|
1602
|
+
downColor: mergedOptions.downColor
|
|
1603
|
+
},
|
|
1604
|
+
indicator.inputs
|
|
1605
|
+
);
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1031
1608
|
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1032
1609
|
if (crosshair.visible && crosshairPoint) {
|
|
1033
1610
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -1051,11 +1628,56 @@ function createChart(element, options = {}) {
|
|
|
1051
1628
|
ctx.restore();
|
|
1052
1629
|
}
|
|
1053
1630
|
ctx.restore();
|
|
1631
|
+
if (activeSeparateIndicators.length > 0) {
|
|
1632
|
+
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1633
|
+
let paneTopCursor = chartBottom + paneGap;
|
|
1634
|
+
activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
|
|
1635
|
+
const paneHeight = separatePaneHeights[paneIndex] ?? 80;
|
|
1636
|
+
const paneTop = paneTopCursor;
|
|
1637
|
+
const paneBottom = paneTop + paneHeight;
|
|
1638
|
+
paneTopCursor = paneBottom + separatePaneSpacing;
|
|
1639
|
+
ctx.save();
|
|
1640
|
+
ctx.beginPath();
|
|
1641
|
+
ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
|
|
1642
|
+
ctx.clip();
|
|
1643
|
+
plugin.draw(
|
|
1644
|
+
ctx,
|
|
1645
|
+
{
|
|
1646
|
+
data,
|
|
1647
|
+
startIndex,
|
|
1648
|
+
endIndex,
|
|
1649
|
+
xStart,
|
|
1650
|
+
xSpan,
|
|
1651
|
+
chartLeft,
|
|
1652
|
+
chartRight,
|
|
1653
|
+
chartTop: paneTop,
|
|
1654
|
+
chartBottom: paneBottom,
|
|
1655
|
+
chartWidth,
|
|
1656
|
+
chartHeight: paneHeight,
|
|
1657
|
+
xFromIndex,
|
|
1658
|
+
yFromPrice: null,
|
|
1659
|
+
candleSpacing,
|
|
1660
|
+
upColor: mergedOptions.upColor,
|
|
1661
|
+
downColor: mergedOptions.downColor
|
|
1662
|
+
},
|
|
1663
|
+
indicator.inputs
|
|
1664
|
+
);
|
|
1665
|
+
ctx.restore();
|
|
1666
|
+
ctx.save();
|
|
1667
|
+
ctx.strokeStyle = axis.lineColor;
|
|
1668
|
+
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
1669
|
+
ctx.beginPath();
|
|
1670
|
+
ctx.moveTo(crisp(chartLeft), crisp(paneTop));
|
|
1671
|
+
ctx.lineTo(crisp(chartRight), crisp(paneTop));
|
|
1672
|
+
ctx.stroke();
|
|
1673
|
+
ctx.restore();
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1054
1676
|
ctx.strokeStyle = axis.lineColor;
|
|
1055
1677
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
1056
1678
|
ctx.beginPath();
|
|
1057
|
-
ctx.moveTo(crisp(chartLeft), crisp(
|
|
1058
|
-
ctx.lineTo(crisp(chartRight), crisp(
|
|
1679
|
+
ctx.moveTo(crisp(chartLeft), crisp(fullChartBottom));
|
|
1680
|
+
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
1059
1681
|
ctx.moveTo(crisp(chartRight), crisp(chartTop));
|
|
1060
1682
|
ctx.lineTo(crisp(chartRight), crisp(chartBottom));
|
|
1061
1683
|
ctx.stroke();
|
|
@@ -1119,7 +1741,7 @@ function createChart(element, options = {}) {
|
|
|
1119
1741
|
month: "short",
|
|
1120
1742
|
day: "numeric"
|
|
1121
1743
|
});
|
|
1122
|
-
drawText(timeLabel, x,
|
|
1744
|
+
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", axis.textColor);
|
|
1123
1745
|
}
|
|
1124
1746
|
if (crosshair.visible && crosshairPoint) {
|
|
1125
1747
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -1243,7 +1865,7 @@ function createChart(element, options = {}) {
|
|
|
1243
1865
|
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
1244
1866
|
const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
|
|
1245
1867
|
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
1246
|
-
const timeY =
|
|
1868
|
+
const timeY = fullChartBottom + 8;
|
|
1247
1869
|
ctx.fillStyle = labelBackground;
|
|
1248
1870
|
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, labelHeight, labelRadius);
|
|
1249
1871
|
strokeCrosshairLabel(timeX, timeY, timeWidth);
|
|
@@ -1450,11 +2072,12 @@ function createChart(element, options = {}) {
|
|
|
1450
2072
|
if (!drawState) {
|
|
1451
2073
|
return "outside";
|
|
1452
2074
|
}
|
|
1453
|
-
const
|
|
2075
|
+
const plotBottom = Math.max(drawState.chartBottom, plotBottomForHit);
|
|
2076
|
+
const inPlot = x >= drawState.chartLeft && x <= drawState.chartRight && y >= drawState.chartTop && y <= plotBottom;
|
|
1454
2077
|
if (inPlot) {
|
|
1455
2078
|
return "plot";
|
|
1456
2079
|
}
|
|
1457
|
-
const inXAxis = x >= drawState.chartLeft && x <= drawState.chartRight && y >
|
|
2080
|
+
const inXAxis = x >= drawState.chartLeft && x <= drawState.chartRight && y > plotBottom && y <= height;
|
|
1458
2081
|
if (inXAxis) {
|
|
1459
2082
|
return "x-axis";
|
|
1460
2083
|
}
|
|
@@ -1779,6 +2402,9 @@ function createChart(element, options = {}) {
|
|
|
1779
2402
|
};
|
|
1780
2403
|
const setData = (nextData) => {
|
|
1781
2404
|
const hadData = data.length > 0;
|
|
2405
|
+
const previousCenterRounded = hadData ? Math.round(xCenter) : 0;
|
|
2406
|
+
const previousCenterFraction = hadData ? xCenter - previousCenterRounded : 0;
|
|
2407
|
+
const previousCenterTimeMs = hadData && previousCenterRounded >= 0 && previousCenterRounded < data.length ? data[previousCenterRounded]?.time.getTime() ?? null : null;
|
|
1782
2408
|
data = parseData(nextData);
|
|
1783
2409
|
if (data.length === 0) {
|
|
1784
2410
|
xCenter = 0;
|
|
@@ -1792,6 +2418,12 @@ function createChart(element, options = {}) {
|
|
|
1792
2418
|
resetYViewport();
|
|
1793
2419
|
} else {
|
|
1794
2420
|
if (mergedOptions.preserveViewportOnDataUpdate) {
|
|
2421
|
+
if (previousCenterTimeMs !== null) {
|
|
2422
|
+
const nextCenter = findNearestIndexForTimeMs(previousCenterTimeMs);
|
|
2423
|
+
if (nextCenter !== null) {
|
|
2424
|
+
xCenter = nextCenter + previousCenterFraction;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
1795
2427
|
clampXViewport();
|
|
1796
2428
|
} else {
|
|
1797
2429
|
fitXViewport();
|
|
@@ -1868,6 +2500,60 @@ function createChart(element, options = {}) {
|
|
|
1868
2500
|
const setDoubleClickAction = (action) => {
|
|
1869
2501
|
doubleClickAction = action;
|
|
1870
2502
|
};
|
|
2503
|
+
const registerIndicator = (plugin) => {
|
|
2504
|
+
if (!plugin.id || typeof plugin.draw !== "function") {
|
|
2505
|
+
throw new Error("Invalid indicator plugin. Expected { id, draw }.");
|
|
2506
|
+
}
|
|
2507
|
+
indicatorRegistry.set(plugin.id, plugin);
|
|
2508
|
+
draw();
|
|
2509
|
+
};
|
|
2510
|
+
const unregisterIndicator = (type) => {
|
|
2511
|
+
indicatorRegistry.delete(type);
|
|
2512
|
+
indicators = indicators.filter((indicator) => indicator.type !== type);
|
|
2513
|
+
draw();
|
|
2514
|
+
};
|
|
2515
|
+
const setIndicators = (nextIndicators) => {
|
|
2516
|
+
indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
|
|
2517
|
+
draw();
|
|
2518
|
+
};
|
|
2519
|
+
const addIndicator = (type, inputs = {}, options2 = {}) => {
|
|
2520
|
+
const plugin = indicatorRegistry.get(type);
|
|
2521
|
+
if (!plugin) {
|
|
2522
|
+
throw new Error(`Unknown indicator type "${type}". Register it first.`);
|
|
2523
|
+
}
|
|
2524
|
+
const next = normalizeIndicatorState({
|
|
2525
|
+
...options2,
|
|
2526
|
+
type,
|
|
2527
|
+
inputs
|
|
2528
|
+
});
|
|
2529
|
+
indicators.push(next);
|
|
2530
|
+
draw();
|
|
2531
|
+
return next.id;
|
|
2532
|
+
};
|
|
2533
|
+
const updateIndicator = (id, patch) => {
|
|
2534
|
+
indicators = indicators.map((indicator) => {
|
|
2535
|
+
if (indicator.id !== id) {
|
|
2536
|
+
return indicator;
|
|
2537
|
+
}
|
|
2538
|
+
const plugin = indicatorRegistry.get(indicator.type);
|
|
2539
|
+
return {
|
|
2540
|
+
...indicator,
|
|
2541
|
+
visible: patch.visible ?? indicator.visible,
|
|
2542
|
+
pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
|
|
2543
|
+
...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
|
|
2544
|
+
type: patch.type ?? indicator.type,
|
|
2545
|
+
inputs: {
|
|
2546
|
+
...indicator.inputs,
|
|
2547
|
+
...patch.inputs ?? {}
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
});
|
|
2551
|
+
draw();
|
|
2552
|
+
};
|
|
2553
|
+
const removeIndicator = (id) => {
|
|
2554
|
+
indicators = indicators.filter((indicator) => indicator.id !== id);
|
|
2555
|
+
draw();
|
|
2556
|
+
};
|
|
1871
2557
|
const destroy = () => {
|
|
1872
2558
|
canvas.removeEventListener("pointerdown", onPointerDown);
|
|
1873
2559
|
canvas.removeEventListener("pointermove", onPointerMove);
|
|
@@ -1902,6 +2588,12 @@ function createChart(element, options = {}) {
|
|
|
1902
2588
|
fitContent,
|
|
1903
2589
|
setDoubleClickEnabled,
|
|
1904
2590
|
setDoubleClickAction,
|
|
2591
|
+
registerIndicator,
|
|
2592
|
+
unregisterIndicator,
|
|
2593
|
+
addIndicator,
|
|
2594
|
+
updateIndicator,
|
|
2595
|
+
removeIndicator,
|
|
2596
|
+
setIndicators,
|
|
1905
2597
|
resize,
|
|
1906
2598
|
destroy
|
|
1907
2599
|
};
|