hyperprop-charting-library 0.1.20 → 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.
@@ -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 chartHeight = height - margin.top - margin.bottom;
809
- const chartBottom = chartTop + chartHeight;
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(chartBottom));
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(chartBottom));
1034
- ctx.lineTo(crisp(chartRight), crisp(chartBottom));
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, chartBottom + 8, "center", "top", axis.textColor);
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 = chartBottom + 8;
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 inPlot = x >= drawState.chartLeft && x <= drawState.chartRight && y >= drawState.chartTop && y <= drawState.chartBottom;
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 > drawState.chartBottom && y <= height;
2051
+ const inXAxis = x >= drawState.chartLeft && x <= drawState.chartRight && y > plotBottom && y <= height;
1434
2052
  if (inXAxis) {
1435
2053
  return "x-axis";
1436
2054
  }
@@ -1755,6 +2373,9 @@ function createChart(element, options = {}) {
1755
2373
  };
1756
2374
  const setData = (nextData) => {
1757
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;
1758
2379
  data = parseData(nextData);
1759
2380
  if (data.length === 0) {
1760
2381
  xCenter = 0;
@@ -1768,6 +2389,12 @@ function createChart(element, options = {}) {
1768
2389
  resetYViewport();
1769
2390
  } else {
1770
2391
  if (mergedOptions.preserveViewportOnDataUpdate) {
2392
+ if (previousCenterTimeMs !== null) {
2393
+ const nextCenter = findNearestIndexForTimeMs(previousCenterTimeMs);
2394
+ if (nextCenter !== null) {
2395
+ xCenter = nextCenter + previousCenterFraction;
2396
+ }
2397
+ }
1771
2398
  clampXViewport();
1772
2399
  } else {
1773
2400
  fitXViewport();
@@ -1844,6 +2471,59 @@ function createChart(element, options = {}) {
1844
2471
  const setDoubleClickAction = (action) => {
1845
2472
  doubleClickAction = action;
1846
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
+ };
1847
2527
  const destroy = () => {
1848
2528
  canvas.removeEventListener("pointerdown", onPointerDown);
1849
2529
  canvas.removeEventListener("pointermove", onPointerMove);
@@ -1878,6 +2558,12 @@ function createChart(element, options = {}) {
1878
2558
  fitContent,
1879
2559
  setDoubleClickEnabled,
1880
2560
  setDoubleClickAction,
2561
+ registerIndicator,
2562
+ unregisterIndicator,
2563
+ addIndicator,
2564
+ updateIndicator,
2565
+ removeIndicator,
2566
+ setIndicators,
1881
2567
  resize,
1882
2568
  destroy
1883
2569
  };