hyperprop-charting-library 0.1.138 → 0.1.140

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/index.js CHANGED
@@ -2821,6 +2821,9 @@ var DEFAULT_OPTIONS = {
2821
2821
  doubleClickAction: "reset",
2822
2822
  keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2823
2823
  accessibility: { label: "Price chart", description: "", focusable: true },
2824
+ downsampling: { enabled: true, thresholdPx: 1.5 },
2825
+ touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
2826
+ datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2824
2827
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2825
2828
  grid: DEFAULT_GRID_OPTIONS,
2826
2829
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2871,6 +2874,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2871
2874
  ...baseOptions.accessibility,
2872
2875
  ...options.accessibility ?? {}
2873
2876
  },
2877
+ downsampling: {
2878
+ ...baseOptions.downsampling,
2879
+ ...options.downsampling ?? {}
2880
+ },
2881
+ touch: {
2882
+ ...baseOptions.touch,
2883
+ ...options.touch ?? {}
2884
+ },
2885
+ datafeedOptions: {
2886
+ ...baseOptions.datafeedOptions,
2887
+ ...options.datafeedOptions ?? {}
2888
+ },
2874
2889
  crosshair: {
2875
2890
  ...baseOptions.crosshair,
2876
2891
  ...options.crosshair ?? {}
@@ -3045,6 +3060,7 @@ function createChart(element, options = {}) {
3045
3060
  let drawingDoubleClickHandler = null;
3046
3061
  let drawingEditTextHandler = null;
3047
3062
  let selectionChangeHandler = null;
3063
+ let longPressHandler = null;
3048
3064
  let contextMenuHandler = null;
3049
3065
  let keyboardShortcutHandler = null;
3050
3066
  let lastSelectionSignature = null;
@@ -3746,6 +3762,150 @@ function createChart(element, options = {}) {
3746
3762
  heikinAshiFingerprint = fingerprint;
3747
3763
  return result;
3748
3764
  };
3765
+ let compareSeriesStates = [];
3766
+ const prepareCompareSeries = (options2) => {
3767
+ const rows = [];
3768
+ for (const point of options2.data ?? []) {
3769
+ const t = new Date(point.t).getTime();
3770
+ const c = Number(point.c);
3771
+ if (Number.isFinite(t) && Number.isFinite(c)) rows.push({ t, c });
3772
+ }
3773
+ rows.sort((a, b) => a.t - b.t);
3774
+ return {
3775
+ options: options2,
3776
+ times: rows.map((row) => row.t),
3777
+ closes: rows.map((row) => row.c),
3778
+ aligned: null,
3779
+ alignedFingerprint: ""
3780
+ };
3781
+ };
3782
+ const mainDataFingerprint = () => {
3783
+ const last = data[data.length - 1];
3784
+ return last ? `${data.length}|${data[0].time.getTime()}|${last.time.getTime()}` : "empty";
3785
+ };
3786
+ const getAlignedCompareValues = (state) => {
3787
+ const fingerprint = `${mainDataFingerprint()}|${state.times.length}|${state.times[state.times.length - 1] ?? 0}`;
3788
+ if (state.aligned && state.alignedFingerprint === fingerprint) {
3789
+ return state.aligned;
3790
+ }
3791
+ const aligned = new Float64Array(data.length).fill(Number.NaN);
3792
+ let cursor = 0;
3793
+ let lastValue = Number.NaN;
3794
+ for (let i = 0; i < data.length; i += 1) {
3795
+ const barTime = data[i].time.getTime();
3796
+ while (cursor < state.times.length && state.times[cursor] <= barTime) {
3797
+ lastValue = state.closes[cursor];
3798
+ cursor += 1;
3799
+ }
3800
+ aligned[i] = lastValue;
3801
+ }
3802
+ state.aligned = aligned;
3803
+ state.alignedFingerprint = fingerprint;
3804
+ return aligned;
3805
+ };
3806
+ const visibleCompareSeries = () => compareSeriesStates.filter((state) => state.options.visible !== false && state.times.length > 0);
3807
+ const setCompareSeries = (series) => {
3808
+ compareSeriesStates = (series ?? []).filter((entry) => entry && entry.id).map(prepareCompareSeries);
3809
+ scheduleDraw({ updateAutoScale: true });
3810
+ };
3811
+ const addCompareSeries = (series) => {
3812
+ if (!series?.id) return;
3813
+ compareSeriesStates = [...compareSeriesStates.filter((state) => state.options.id !== series.id), prepareCompareSeries(series)];
3814
+ scheduleDraw({ updateAutoScale: true });
3815
+ };
3816
+ const removeCompareSeries = (id) => {
3817
+ const next = compareSeriesStates.filter((state) => state.options.id !== id);
3818
+ if (next.length === compareSeriesStates.length) return;
3819
+ compareSeriesStates = next;
3820
+ scheduleDraw({ updateAutoScale: true });
3821
+ };
3822
+ const getCompareSeries = () => compareSeriesStates.map((state) => state.options);
3823
+ let datafeed = null;
3824
+ let datafeedUnsubscribe = null;
3825
+ let datafeedLoading = false;
3826
+ let datafeedExhausted = false;
3827
+ let datafeedLastRequestAt = 0;
3828
+ const estimateBarStepMs = () => {
3829
+ if (mergedOptions.timeStepMs > 0) return mergedOptions.timeStepMs;
3830
+ if (data.length < 2) return 6e4;
3831
+ const span = data[data.length - 1].time.getTime() - data[0].time.getTime();
3832
+ return Math.max(1e3, span / (data.length - 1));
3833
+ };
3834
+ const requestDatafeedBars = async (request) => {
3835
+ if (!datafeed) return [];
3836
+ try {
3837
+ return await datafeed.getBars(request) ?? [];
3838
+ } catch (error) {
3839
+ datafeed.onError?.(error, request);
3840
+ return [];
3841
+ }
3842
+ };
3843
+ const maybeLoadOlderHistory = () => {
3844
+ if (!datafeed || datafeedLoading || datafeedExhausted || data.length === 0) return;
3845
+ const options2 = mergedOptions.datafeedOptions ?? DEFAULT_OPTIONS.datafeedOptions;
3846
+ const threshold = options2?.prefetchThresholdBars ?? 150;
3847
+ const cooldown = options2?.cooldownMs ?? 1200;
3848
+ if (Date.now() - datafeedLastRequestAt < cooldown) return;
3849
+ if (xCenter - xSpan / 2 > threshold) return;
3850
+ const stepMs = estimateBarStepMs();
3851
+ const chunk = Math.max(10, options2?.chunkBars ?? 1500);
3852
+ const toMs = data[0].time.getTime();
3853
+ datafeedLoading = true;
3854
+ datafeedLastRequestAt = Date.now();
3855
+ void requestDatafeedBars({
3856
+ fromMs: toMs - chunk * stepMs,
3857
+ toMs,
3858
+ countBack: chunk,
3859
+ firstRequest: false
3860
+ }).then((bars) => {
3861
+ const older = bars.filter((bar) => new Date(bar.t).getTime() < toMs);
3862
+ if (older.length === 0) {
3863
+ datafeedExhausted = true;
3864
+ return;
3865
+ }
3866
+ const merged = [...older, ...data.map((bar) => ({
3867
+ t: bar.time.toISOString(),
3868
+ o: bar.o,
3869
+ h: bar.h,
3870
+ l: bar.l,
3871
+ c: bar.c,
3872
+ ...bar.v === void 0 ? {} : { v: bar.v }
3873
+ }))];
3874
+ setData(merged);
3875
+ }).finally(() => {
3876
+ datafeedLoading = false;
3877
+ });
3878
+ };
3879
+ const setDatafeed = async (nextDatafeed) => {
3880
+ datafeedUnsubscribe?.();
3881
+ datafeedUnsubscribe = null;
3882
+ datafeed = nextDatafeed;
3883
+ datafeedExhausted = false;
3884
+ datafeedLoading = false;
3885
+ datafeedLastRequestAt = 0;
3886
+ if (!nextDatafeed) return;
3887
+ await nextDatafeed.onReady?.();
3888
+ if (datafeed !== nextDatafeed) return;
3889
+ const stepMs = estimateBarStepMs();
3890
+ const countBack = Math.max(50, Math.round(mergedOptions.initialVisibleBars * 3));
3891
+ const toMs = Date.now();
3892
+ datafeedLoading = true;
3893
+ const bars = await requestDatafeedBars({
3894
+ fromMs: toMs - countBack * stepMs,
3895
+ toMs,
3896
+ countBack,
3897
+ firstRequest: true
3898
+ });
3899
+ datafeedLoading = false;
3900
+ if (datafeed !== nextDatafeed) return;
3901
+ if (bars.length > 0) {
3902
+ setData(bars);
3903
+ }
3904
+ const unsubscribe = nextDatafeed.subscribeBars?.((bar) => {
3905
+ if (datafeed === nextDatafeed) upsertBar(bar);
3906
+ });
3907
+ datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
3908
+ };
3749
3909
  const formatHoverTimeLabel = (time, mode) => {
3750
3910
  if (mode === "time") {
3751
3911
  return time.toLocaleTimeString(void 0, {
@@ -4351,6 +4511,31 @@ function createChart(element, options = {}) {
4351
4511
  const chartType = mergedOptions.chartType;
4352
4512
  const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
4353
4513
  const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
4514
+ const comparePlans = [];
4515
+ for (const state of visibleCompareSeries()) {
4516
+ const aligned = getAlignedCompareValues(state);
4517
+ let anchorIndex = -1;
4518
+ for (let index = startIndex; index <= endIndex; index += 1) {
4519
+ if (Number.isFinite(aligned[index] ?? Number.NaN)) {
4520
+ anchorIndex = index;
4521
+ break;
4522
+ }
4523
+ }
4524
+ if (anchorIndex < 0) continue;
4525
+ const baseCompare = aligned[anchorIndex];
4526
+ const baseMain = seriesData[anchorIndex]?.c ?? 0;
4527
+ const usePercent = (state.options.scale ?? "percent") !== "price";
4528
+ const toPrice = usePercent && baseCompare > 0 ? (value) => baseMain * value / baseCompare : (value) => value;
4529
+ let lastNativeValue = Number.NaN;
4530
+ for (let index = endIndex; index >= startIndex; index -= 1) {
4531
+ const value = aligned[index];
4532
+ if (value !== void 0 && Number.isFinite(value)) {
4533
+ lastNativeValue = value;
4534
+ break;
4535
+ }
4536
+ }
4537
+ comparePlans.push({ state, aligned, toPrice, usePercent, baseCompare, lastNativeValue });
4538
+ }
4354
4539
  const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
4355
4540
  const scanCandleRange = (from, to) => {
4356
4541
  let min = Number.POSITIVE_INFINITY;
@@ -4396,6 +4581,23 @@ function createChart(element, options = {}) {
4396
4581
  maxPrice = Math.max(maxPrice, weightedMax);
4397
4582
  }
4398
4583
  }
4584
+ for (const plan of comparePlans) {
4585
+ if (plan.state.options.includeInAutoScale === false) continue;
4586
+ let compareMin = Number.POSITIVE_INFINITY;
4587
+ let compareMax = Number.NEGATIVE_INFINITY;
4588
+ for (let index = startIndex; index <= endIndex; index += 1) {
4589
+ const value = plan.aligned[index];
4590
+ if (value === void 0 || !Number.isFinite(value)) continue;
4591
+ const price = plan.toPrice(value);
4592
+ if (!Number.isFinite(price)) continue;
4593
+ if (price < compareMin) compareMin = price;
4594
+ if (price > compareMax) compareMax = price;
4595
+ }
4596
+ if (compareMin <= compareMax) {
4597
+ minPrice = Math.min(minPrice, compareMin);
4598
+ maxPrice = Math.max(maxPrice, compareMax);
4599
+ }
4600
+ }
4399
4601
  const priceRange = maxPrice - minPrice || 1;
4400
4602
  const autoMin = minPrice - priceRange * 0.08;
4401
4603
  const autoMax = maxPrice + priceRange * 0.08;
@@ -5507,7 +5709,106 @@ function createChart(element, options = {}) {
5507
5709
  }
5508
5710
  return data[index]?.v;
5509
5711
  };
5510
- if (isLineStyleChart) {
5712
+ const downsamplingOptions = mergedOptions.downsampling ?? DEFAULT_OPTIONS.downsampling;
5713
+ const downsampleThresholdPx = Math.max(0.2, downsamplingOptions?.thresholdPx ?? 1.5);
5714
+ const shouldDownsample = (downsamplingOptions?.enabled ?? true) && candleSpacing < downsampleThresholdPx && endIndex - startIndex > 2;
5715
+ const seriesColumns = [];
5716
+ if (shouldDownsample) {
5717
+ let current = null;
5718
+ let currentColumn = Number.NaN;
5719
+ for (let index = startIndex; index <= endIndex; index += 1) {
5720
+ const point = seriesData[index];
5721
+ if (!point) continue;
5722
+ const column = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
5723
+ if (!current || column !== currentColumn) {
5724
+ currentColumn = column;
5725
+ current = {
5726
+ x: column,
5727
+ open: point.o,
5728
+ high: point.h,
5729
+ low: point.l,
5730
+ close: point.c,
5731
+ minClose: point.c,
5732
+ maxClose: point.c,
5733
+ minCloseIndex: index,
5734
+ maxCloseIndex: index
5735
+ };
5736
+ seriesColumns.push(current);
5737
+ continue;
5738
+ }
5739
+ if (point.h > current.high) current.high = point.h;
5740
+ if (point.l < current.low) current.low = point.l;
5741
+ current.close = point.c;
5742
+ if (point.c < current.minClose) {
5743
+ current.minClose = point.c;
5744
+ current.minCloseIndex = index;
5745
+ }
5746
+ if (point.c > current.maxClose) {
5747
+ current.maxClose = point.c;
5748
+ current.maxCloseIndex = index;
5749
+ }
5750
+ }
5751
+ }
5752
+ if (shouldDownsample && !isLineStyleChart) {
5753
+ const upColumns = new Path2D();
5754
+ const downColumns = new Path2D();
5755
+ for (const column of seriesColumns) {
5756
+ const path = column.close >= column.open ? upColumns : downColumns;
5757
+ const x = column.x + 0.5;
5758
+ const top = crisp(yFromPrice(column.high));
5759
+ const bottom = crisp(yFromPrice(column.low));
5760
+ path.moveTo(x, top);
5761
+ path.lineTo(x, bottom === top ? bottom + 1 : bottom);
5762
+ }
5763
+ ctx.lineWidth = 1;
5764
+ ctx.strokeStyle = mergedOptions.upColor;
5765
+ ctx.stroke(upColumns);
5766
+ ctx.strokeStyle = mergedOptions.downColor;
5767
+ ctx.stroke(downColumns);
5768
+ } else if (shouldDownsample && isLineStyleChart) {
5769
+ const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5770
+ const linePath = new Path2D();
5771
+ let started = false;
5772
+ let firstX = 0;
5773
+ let lastX = 0;
5774
+ for (const column of seriesColumns) {
5775
+ const lowFirst = column.minCloseIndex <= column.maxCloseIndex;
5776
+ const first = lowFirst ? column.minClose : column.maxClose;
5777
+ const second = lowFirst ? column.maxClose : column.minClose;
5778
+ const x = column.x;
5779
+ if (!started) {
5780
+ linePath.moveTo(x, yFromPrice(first));
5781
+ firstX = x;
5782
+ started = true;
5783
+ } else {
5784
+ linePath.lineTo(x, yFromPrice(first));
5785
+ }
5786
+ if (second !== first) {
5787
+ linePath.lineTo(x, yFromPrice(second));
5788
+ }
5789
+ lastX = x;
5790
+ }
5791
+ if (started) {
5792
+ if (chartType === "area" || chartType === "baseline") {
5793
+ const baseY = chartType === "baseline" ? yFromPrice(mergedOptions.baselinePrice ?? (yMin + yMax) / 2) : chartBottom;
5794
+ const fillPath = new Path2D(linePath);
5795
+ fillPath.lineTo(lastX, baseY);
5796
+ fillPath.lineTo(firstX, baseY);
5797
+ fillPath.closePath();
5798
+ ctx.save();
5799
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
5800
+ ctx.fillStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5801
+ ctx.fill(fillPath);
5802
+ ctx.restore();
5803
+ }
5804
+ ctx.save();
5805
+ ctx.strokeStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5806
+ ctx.lineWidth = seriesLineWidth;
5807
+ ctx.lineJoin = "round";
5808
+ ctx.stroke(linePath);
5809
+ ctx.restore();
5810
+ }
5811
+ } else if (isLineStyleChart) {
5511
5812
  const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5512
5813
  const linePath = new Path2D();
5513
5814
  let started = false;
@@ -5673,6 +5974,105 @@ function createChart(element, options = {}) {
5673
5974
  ctx.stroke(downHollowBodies);
5674
5975
  }
5675
5976
  }
5977
+ const compareAxisTags = [];
5978
+ for (const plan of comparePlans) {
5979
+ const color = plan.state.options.color ?? "#f59e0b";
5980
+ const lineWidth = Math.max(1, plan.state.options.lineWidth ?? 1.5);
5981
+ const path = new Path2D();
5982
+ let started = false;
5983
+ let firstX = 0;
5984
+ let lastX = 0;
5985
+ let lastY = 0;
5986
+ let lastPrice = Number.NaN;
5987
+ const emit = (x, y) => {
5988
+ if (!started) {
5989
+ path.moveTo(x, y);
5990
+ firstX = x;
5991
+ started = true;
5992
+ } else {
5993
+ path.lineTo(x, y);
5994
+ }
5995
+ lastX = x;
5996
+ lastY = y;
5997
+ };
5998
+ const xForIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
5999
+ if (shouldDownsample) {
6000
+ let column = Number.NaN;
6001
+ let minValue = 0;
6002
+ let maxValue = 0;
6003
+ let minIndex = 0;
6004
+ let maxIndex = 0;
6005
+ let hasColumn = false;
6006
+ const flushColumn = () => {
6007
+ if (!hasColumn) return;
6008
+ const lowFirst = minIndex <= maxIndex;
6009
+ const first = plan.toPrice(lowFirst ? minValue : maxValue);
6010
+ const second = plan.toPrice(lowFirst ? maxValue : minValue);
6011
+ if (Number.isFinite(first)) emit(column, yFromPrice(first));
6012
+ if (Number.isFinite(second) && second !== first) emit(column, yFromPrice(second));
6013
+ lastPrice = plan.toPrice(maxIndex >= minIndex ? maxValue : minValue);
6014
+ };
6015
+ for (let index = startIndex; index <= endIndex; index += 1) {
6016
+ const value = plan.aligned[index];
6017
+ if (value === void 0 || !Number.isFinite(value)) continue;
6018
+ const nextColumn = Math.round(xForIndex(index));
6019
+ if (!hasColumn || nextColumn !== column) {
6020
+ flushColumn();
6021
+ column = nextColumn;
6022
+ minValue = value;
6023
+ maxValue = value;
6024
+ minIndex = index;
6025
+ maxIndex = index;
6026
+ hasColumn = true;
6027
+ continue;
6028
+ }
6029
+ if (value < minValue) {
6030
+ minValue = value;
6031
+ minIndex = index;
6032
+ }
6033
+ if (value > maxValue) {
6034
+ maxValue = value;
6035
+ maxIndex = index;
6036
+ }
6037
+ }
6038
+ flushColumn();
6039
+ } else {
6040
+ for (let index = startIndex; index <= endIndex; index += 1) {
6041
+ const value = plan.aligned[index];
6042
+ if (value === void 0 || !Number.isFinite(value)) continue;
6043
+ const price = plan.toPrice(value);
6044
+ if (!Number.isFinite(price)) continue;
6045
+ lastPrice = price;
6046
+ emit(xForIndex(index), yFromPrice(price));
6047
+ }
6048
+ }
6049
+ if (!started) continue;
6050
+ if (plan.state.options.style === "area") {
6051
+ const fillPath = new Path2D(path);
6052
+ fillPath.lineTo(lastX, chartBottom);
6053
+ fillPath.lineTo(firstX, chartBottom);
6054
+ fillPath.closePath();
6055
+ ctx.save();
6056
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity)) * 0.6;
6057
+ ctx.fillStyle = color;
6058
+ ctx.fill(fillPath);
6059
+ ctx.restore();
6060
+ }
6061
+ ctx.save();
6062
+ ctx.strokeStyle = color;
6063
+ ctx.lineWidth = lineWidth;
6064
+ ctx.lineJoin = "round";
6065
+ ctx.stroke(path);
6066
+ ctx.restore();
6067
+ if (Number.isFinite(lastPrice)) {
6068
+ const label = plan.state.options.label ?? plan.state.options.id;
6069
+ const nativeLast = plan.lastNativeValue;
6070
+ const usePercentTag = plan.usePercent && plan.baseCompare > 0 && Number.isFinite(nativeLast);
6071
+ const changePct = usePercentTag ? (nativeLast / plan.baseCompare - 1) * 100 : 0;
6072
+ const shortText = usePercentTag ? `${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%` : formatPrice(nativeLast);
6073
+ compareAxisTags.push({ y: lastY, text: `${label} ${shortText}`, shortText, color });
6074
+ }
6075
+ }
5676
6076
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
5677
6077
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
5678
6078
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
@@ -6447,6 +6847,35 @@ function createChart(element, options = {}) {
6447
6847
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
6448
6848
  ctx.font = prevFont;
6449
6849
  }
6850
+ if (compareAxisTags.length > 0 && width - chartRight > 0) {
6851
+ const prevFont = ctx.font;
6852
+ const tagFontSize = Math.max(9, (resolvedAxis.fontSize ?? 12) - 2);
6853
+ ctx.font = `${tagFontSize}px ${mergedOptions.fontFamily}`;
6854
+ ctx.textBaseline = "middle";
6855
+ ctx.textAlign = "left";
6856
+ const placed = [];
6857
+ const tagHeight = tagFontSize + 6;
6858
+ const gutterWidth = width - chartRight - 4;
6859
+ for (const tag of [...compareAxisTags].sort((a, b) => a.y - b.y)) {
6860
+ const text = measureTextWidth(tag.text) + 10 <= gutterWidth ? tag.text : tag.shortText;
6861
+ const tagWidth = Math.min(gutterWidth, measureTextWidth(text) + 10);
6862
+ if (tagWidth <= 0) continue;
6863
+ let tagY = clamp(tag.y - tagHeight / 2, chartTop, chartBottom - tagHeight);
6864
+ for (const slot of placed) {
6865
+ if (tagY < slot.bottom + 2 && tagY + tagHeight > slot.top - 2) {
6866
+ tagY = slot.bottom + 2;
6867
+ }
6868
+ }
6869
+ placed.push({ top: tagY, bottom: tagY + tagHeight });
6870
+ ctx.save();
6871
+ ctx.fillStyle = tag.color;
6872
+ fillRoundedRect(chartRight + 2, Math.round(tagY), Math.round(tagWidth), tagHeight, 3);
6873
+ ctx.fillStyle = labelTextColorOn(tag.color, "#ffffff");
6874
+ ctx.fillText(text, chartRight + 7, Math.round(tagY + tagHeight / 2));
6875
+ ctx.restore();
6876
+ }
6877
+ ctx.font = prevFont;
6878
+ }
6450
6879
  if (baseCtx) {
6451
6880
  if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
6452
6881
  baseCanvas.width = canvas.width;
@@ -6471,6 +6900,7 @@ function createChart(element, options = {}) {
6471
6900
  };
6472
6901
  paintCrosshairOverlay();
6473
6902
  emitSelectionChangeIfMoved();
6903
+ maybeLoadOlderHistory();
6474
6904
  };
6475
6905
  const paintCrosshairOverlay = () => {
6476
6906
  crosshairPriceActionRegion = null;
@@ -6663,6 +7093,67 @@ function createChart(element, options = {}) {
6663
7093
  drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
6664
7094
  }
6665
7095
  }
7096
+ if (longPressState && (mergedOptions.touch?.longPressTooltip ?? true)) {
7097
+ const index = indexFromCanvasX(cx);
7098
+ const bar = index === null ? void 0 : data[index];
7099
+ if (bar) {
7100
+ const barTime = getTimeForIndex(index);
7101
+ const changeAbs = bar.c - bar.o;
7102
+ const changePct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
7103
+ const rows = [
7104
+ ["O", formatPrice(bar.o), null],
7105
+ ["H", formatPrice(bar.h), null],
7106
+ ["L", formatPrice(bar.l), null],
7107
+ ["C", formatPrice(bar.c), null],
7108
+ [
7109
+ "Chg",
7110
+ `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${changePct.toFixed(2)}%)`,
7111
+ changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor
7112
+ ]
7113
+ ];
7114
+ if (bar.v !== void 0) {
7115
+ rows.push([
7116
+ "Vol",
7117
+ bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v)),
7118
+ null
7119
+ ]);
7120
+ }
7121
+ const prevFont = ctx.font;
7122
+ const fontSize = 11;
7123
+ ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
7124
+ const headerText = barTime ? formatHoverTimeLabel(barTime, "auto") : "";
7125
+ const rowGap = 3;
7126
+ const padding = 8;
7127
+ const labelColumn = 26;
7128
+ const contentWidth = Math.max(
7129
+ measureTextWidth(headerText),
7130
+ ...rows.map(([, value]) => labelColumn + measureTextWidth(value))
7131
+ );
7132
+ const boxWidth = Math.ceil(contentWidth) + padding * 2;
7133
+ const boxHeight = padding * 2 + (fontSize + rowGap) * (rows.length + (headerText ? 1 : 0)) - rowGap;
7134
+ const fingerGap = 26;
7135
+ const boxX = cx + fingerGap + boxWidth <= chartRight ? cx + fingerGap : Math.max(chartLeft + 4, cx - fingerGap - boxWidth);
7136
+ const boxY = clamp(cy - boxHeight - fingerGap, chartTop + 4, chartBottom - boxHeight - 4);
7137
+ ctx.save();
7138
+ ctx.fillStyle = "rgba(16,17,20,0.94)";
7139
+ fillRoundedRect(Math.round(boxX), Math.round(boxY), boxWidth, boxHeight, 6);
7140
+ ctx.strokeStyle = "rgba(255,255,255,0.14)";
7141
+ ctx.lineWidth = 1;
7142
+ ctx.strokeRect(Math.round(boxX) + 0.5, Math.round(boxY) + 0.5, boxWidth - 1, boxHeight - 1);
7143
+ let textY = boxY + padding + fontSize / 2;
7144
+ if (headerText) {
7145
+ drawText(headerText, boxX + padding, textY, "left", "middle", "rgba(255,255,255,0.55)");
7146
+ textY += fontSize + rowGap;
7147
+ }
7148
+ for (const [label, value, valueColor] of rows) {
7149
+ drawText(label, boxX + padding, textY, "left", "middle", "rgba(255,255,255,0.5)");
7150
+ drawText(value, boxX + padding + labelColumn, textY, "left", "middle", valueColor ?? "#e6e8ee");
7151
+ textY += fontSize + rowGap;
7152
+ }
7153
+ ctx.restore();
7154
+ ctx.font = prevFont;
7155
+ }
7156
+ }
6666
7157
  };
6667
7158
  const drawOverlayOnly = () => {
6668
7159
  if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
@@ -7172,6 +7663,7 @@ function createChart(element, options = {}) {
7172
7663
  return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy));
7173
7664
  };
7174
7665
  const getDrawingHit = (x, y) => {
7666
+ const hitTolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
7175
7667
  for (let index = drawings.length - 1; index >= 0; index -= 1) {
7176
7668
  const drawing = drawings[index];
7177
7669
  if (!drawing?.visible) continue;
@@ -7180,10 +7672,10 @@ function createChart(element, options = {}) {
7180
7672
  if (!point) continue;
7181
7673
  const lineY = canvasYFromDrawingPrice(point.price);
7182
7674
  const handleX = drawState ? drawState.chartRight - 64 : 0;
7183
- if (Math.hypot(x - handleX, y - lineY) <= 8) {
7675
+ if (Math.hypot(x - handleX, y - lineY) <= 8 * hitTolerance) {
7184
7676
  return { drawing, target: "handle", pointIndex: 0 };
7185
7677
  }
7186
- if (Math.abs(y - lineY) <= 7) {
7678
+ if (Math.abs(y - lineY) <= 7 * hitTolerance) {
7187
7679
  return { drawing, target: "line" };
7188
7680
  }
7189
7681
  } else if (drawing.type === "vertical-line") {
@@ -7191,10 +7683,10 @@ function createChart(element, options = {}) {
7191
7683
  if (!point) continue;
7192
7684
  const lineX = canvasXFromDrawingPoint(point);
7193
7685
  const handleY = drawState ? (drawState.chartTop + drawState.chartBottom) / 2 : 0;
7194
- if (Math.hypot(x - lineX, y - handleY) <= 8) {
7686
+ if (Math.hypot(x - lineX, y - handleY) <= 8 * hitTolerance) {
7195
7687
  return { drawing, target: "handle", pointIndex: 0 };
7196
7688
  }
7197
- if (Math.abs(x - lineX) <= 7) {
7689
+ if (Math.abs(x - lineX) <= 7 * hitTolerance) {
7198
7690
  return { drawing, target: "line" };
7199
7691
  }
7200
7692
  } else if (drawing.type === "trendline") {
@@ -7205,13 +7697,13 @@ function createChart(element, options = {}) {
7205
7697
  const y1 = canvasYFromDrawingPrice(first.price);
7206
7698
  const x2 = canvasXFromDrawingPoint(second);
7207
7699
  const y2 = canvasYFromDrawingPrice(second.price);
7208
- if (Math.hypot(x - x1, y - y1) <= 8) {
7700
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7209
7701
  return { drawing, target: "handle", pointIndex: 0 };
7210
7702
  }
7211
- if (Math.hypot(x - x2, y - y2) <= 8) {
7703
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7212
7704
  return { drawing, target: "handle", pointIndex: 1 };
7213
7705
  }
7214
- if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7706
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7215
7707
  return { drawing, target: "line" };
7216
7708
  }
7217
7709
  } else if (drawing.type === "ray") {
@@ -7222,10 +7714,10 @@ function createChart(element, options = {}) {
7222
7714
  const y1 = canvasYFromDrawingPrice(first.price);
7223
7715
  const x2 = canvasXFromDrawingPoint(second);
7224
7716
  const y2 = canvasYFromDrawingPrice(second.price);
7225
- if (Math.hypot(x - x1, y - y1) <= 8) {
7717
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7226
7718
  return { drawing, target: "handle", pointIndex: 0 };
7227
7719
  }
7228
- if (Math.hypot(x - x2, y - y2) <= 8) {
7720
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7229
7721
  return { drawing, target: "handle", pointIndex: 1 };
7230
7722
  }
7231
7723
  const dx = x2 - x1;
@@ -7235,7 +7727,7 @@ function createChart(element, options = {}) {
7235
7727
  const t = Math.max(0, ((x - x1) * dx + (y - y1) * dy) / lengthSq);
7236
7728
  const projX = x1 + t * dx;
7237
7729
  const projY = y1 + t * dy;
7238
- if (Math.hypot(x - projX, y - projY) <= 6) {
7730
+ if (Math.hypot(x - projX, y - projY) <= 6 * hitTolerance) {
7239
7731
  return { drawing, target: "line" };
7240
7732
  }
7241
7733
  }
@@ -7247,17 +7739,17 @@ function createChart(element, options = {}) {
7247
7739
  const y1 = canvasYFromDrawingPrice(first.price);
7248
7740
  const x2 = canvasXFromDrawingPoint(second);
7249
7741
  const y2 = canvasYFromDrawingPrice(second.price);
7250
- if (Math.hypot(x - x1, y - y1) <= 9) {
7742
+ if (Math.hypot(x - x1, y - y1) <= 9 * hitTolerance) {
7251
7743
  return { drawing, target: "handle", pointIndex: 0 };
7252
7744
  }
7253
- if (Math.hypot(x - x2, y - y2) <= 9) {
7745
+ if (Math.hypot(x - x2, y - y2) <= 9 * hitTolerance) {
7254
7746
  return { drawing, target: "handle", pointIndex: 1 };
7255
7747
  }
7256
7748
  const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
7257
7749
  for (const ratio of fibRatios) {
7258
7750
  const price = first.price + (second.price - first.price) * (1 - ratio);
7259
7751
  const lineY = canvasYFromDrawingPrice(price);
7260
- if (Math.abs(y - lineY) <= 5) {
7752
+ if (Math.abs(y - lineY) <= 5 * hitTolerance) {
7261
7753
  return { drawing, target: "line" };
7262
7754
  }
7263
7755
  }
@@ -7270,30 +7762,30 @@ function createChart(element, options = {}) {
7270
7762
  const y0 = canvasYFromDrawingPrice(p0.price);
7271
7763
  const x1 = canvasXFromDrawingPoint(p1);
7272
7764
  const y1 = canvasYFromDrawingPrice(p1.price);
7273
- if (Math.hypot(x - x0, y - y0) <= 8) {
7765
+ if (Math.hypot(x - x0, y - y0) <= 8 * hitTolerance) {
7274
7766
  return { drawing, target: "handle", pointIndex: 0 };
7275
7767
  }
7276
- if (Math.hypot(x - x1, y - y1) <= 8) {
7768
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7277
7769
  return { drawing, target: "handle", pointIndex: 1 };
7278
7770
  }
7279
7771
  if (p2) {
7280
7772
  const x2 = canvasXFromDrawingPoint(p2);
7281
7773
  const y2 = canvasYFromDrawingPrice(p2.price);
7282
- if (Math.hypot(x - x2, y - y2) <= 8) {
7774
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7283
7775
  return { drawing, target: "handle", pointIndex: 2 };
7284
7776
  }
7285
7777
  const move = p1.price - p0.price;
7286
7778
  const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
7287
7779
  for (const ratio of fibExtRatios) {
7288
7780
  const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
7289
- if (Math.abs(y - lineY) <= 5) {
7781
+ if (Math.abs(y - lineY) <= 5 * hitTolerance) {
7290
7782
  return { drawing, target: "line" };
7291
7783
  }
7292
7784
  }
7293
- if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7785
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance || distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7294
7786
  return { drawing, target: "line" };
7295
7787
  }
7296
- } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7788
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance) {
7297
7789
  return { drawing, target: "line" };
7298
7790
  }
7299
7791
  } else if (drawing.type === "long-position" || drawing.type === "short-position") {
@@ -7307,10 +7799,10 @@ function createChart(element, options = {}) {
7307
7799
  const entryY = canvasYFromDrawingPrice(entry.price);
7308
7800
  const targetY = canvasYFromDrawingPrice(target.price);
7309
7801
  const stopY = canvasYFromDrawingPrice(stop.price);
7310
- if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7311
- if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7312
- if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7313
- if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7802
+ if (Math.hypot(x - leftX, y - targetY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7803
+ if (Math.hypot(x - leftX, y - entryY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7804
+ if (Math.hypot(x - leftX, y - stopY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7805
+ if (Math.hypot(x - rightX, y - entryY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7314
7806
  const x0 = Math.min(leftX, rightX);
7315
7807
  const x1 = Math.max(leftX, rightX);
7316
7808
  const yTop = Math.min(targetY, stopY, entryY);
@@ -7326,10 +7818,10 @@ function createChart(element, options = {}) {
7326
7818
  const px1 = canvasXFromDrawingPoint(p1);
7327
7819
  const y0 = canvasYFromDrawingPrice(p0.price);
7328
7820
  const y1 = canvasYFromDrawingPrice(p1.price);
7329
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7330
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7331
- if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7332
- if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7821
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7822
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7823
+ if (Math.hypot(x - px0, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7824
+ if (Math.hypot(x - px1, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7333
7825
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
7334
7826
  return { drawing, target: "line" };
7335
7827
  }
@@ -7341,16 +7833,16 @@ function createChart(element, options = {}) {
7341
7833
  const px1 = canvasXFromDrawingPoint(p1);
7342
7834
  const y0 = canvasYFromDrawingPrice(p0.price);
7343
7835
  const y1 = canvasYFromDrawingPrice(p1.price);
7344
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7345
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7346
- if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7347
- if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7836
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7837
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7838
+ if (Math.hypot(x - px0, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7839
+ if (Math.hypot(x - px1, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7348
7840
  const cx = (px0 + px1) / 2;
7349
7841
  const cy = (y0 + y1) / 2;
7350
7842
  const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7351
7843
  const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7352
7844
  const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7353
- if (norm <= 1) {
7845
+ if (norm <= 1 * hitTolerance) {
7354
7846
  return { drawing, target: "line" };
7355
7847
  }
7356
7848
  } else if (drawing.type === "parallel-channel") {
@@ -7362,18 +7854,18 @@ function createChart(element, options = {}) {
7362
7854
  const y0 = canvasYFromDrawingPrice(p0.price);
7363
7855
  const x1 = canvasXFromDrawingPoint(p1);
7364
7856
  const y1 = canvasYFromDrawingPrice(p1.price);
7365
- if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7366
- if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7857
+ if (Math.hypot(x - x0, y - y0) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7858
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7367
7859
  if (p2) {
7368
7860
  const x2 = canvasXFromDrawingPoint(p2);
7369
7861
  const y2 = canvasYFromDrawingPrice(p2.price);
7370
- if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7862
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7371
7863
  const indexSpan = p1.index - p0.index;
7372
7864
  const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7373
7865
  const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7374
7866
  const q0y = canvasYFromDrawingPrice(p0.price + offset);
7375
7867
  const q1y = canvasYFromDrawingPrice(p1.price + offset);
7376
- if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7868
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6 * hitTolerance) {
7377
7869
  return { drawing, target: "line" };
7378
7870
  }
7379
7871
  const minX = Math.min(x0, x1);
@@ -7386,7 +7878,7 @@ function createChart(element, options = {}) {
7386
7878
  return { drawing, target: "line" };
7387
7879
  }
7388
7880
  }
7389
- } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7881
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance) {
7390
7882
  return { drawing, target: "line" };
7391
7883
  }
7392
7884
  } else if (drawing.type === "arrow") {
@@ -7397,13 +7889,13 @@ function createChart(element, options = {}) {
7397
7889
  const y1 = canvasYFromDrawingPrice(first.price);
7398
7890
  const x2 = canvasXFromDrawingPoint(second);
7399
7891
  const y2 = canvasYFromDrawingPrice(second.price);
7400
- if (Math.hypot(x - x1, y - y1) <= 8) {
7892
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7401
7893
  return { drawing, target: "handle", pointIndex: 0 };
7402
7894
  }
7403
- if (Math.hypot(x - x2, y - y2) <= 8) {
7895
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7404
7896
  return { drawing, target: "handle", pointIndex: 1 };
7405
7897
  }
7406
- if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7898
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7407
7899
  return { drawing, target: "line" };
7408
7900
  }
7409
7901
  } else if (drawing.type === "brush") {
@@ -7415,7 +7907,7 @@ function createChart(element, options = {}) {
7415
7907
  for (let i = 1; i < pts.length; i += 1) {
7416
7908
  const nextX = canvasXFromDrawingPoint(pts[i]);
7417
7909
  const nextY = canvasYFromDrawingPrice(pts[i].price);
7418
- if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7910
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7 * hitTolerance) {
7419
7911
  hitBody = true;
7420
7912
  break;
7421
7913
  }
@@ -7433,8 +7925,8 @@ function createChart(element, options = {}) {
7433
7925
  const ay = canvasYFromDrawingPrice(anchor.price);
7434
7926
  const bx = canvasXFromDrawingPoint(boxPoint);
7435
7927
  const by = canvasYFromDrawingPrice(boxPoint.price);
7436
- if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7437
- if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7928
+ if (Math.hypot(x - ax, y - ay) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7929
+ if (Math.hypot(x - bx, y - by) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7438
7930
  const fontSize = Math.max(6, drawing.fontSize);
7439
7931
  const prevFont = ctx.font;
7440
7932
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
@@ -7450,7 +7942,7 @@ function createChart(element, options = {}) {
7450
7942
  if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7451
7943
  return { drawing, target: "line" };
7452
7944
  }
7453
- if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7945
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
7454
7946
  return { drawing, target: "line" };
7455
7947
  }
7456
7948
  } else if (drawing.type === "price-range") {
@@ -7461,8 +7953,8 @@ function createChart(element, options = {}) {
7461
7953
  const px1 = canvasXFromDrawingPoint(p1);
7462
7954
  const y0 = canvasYFromDrawingPrice(p0.price);
7463
7955
  const y1 = canvasYFromDrawingPrice(p1.price);
7464
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7465
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7956
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7957
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7466
7958
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
7467
7959
  return { drawing, target: "line" };
7468
7960
  }
@@ -7471,7 +7963,7 @@ function createChart(element, options = {}) {
7471
7963
  if (!anchor) continue;
7472
7964
  const ax = canvasXFromDrawingPoint(anchor);
7473
7965
  const ay = canvasYFromDrawingPrice(anchor.price);
7474
- if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7966
+ if (Math.hypot(x - ax, y - ay) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7475
7967
  const fontSize = Math.max(6, drawing.fontSize);
7476
7968
  const prevFont = ctx.font;
7477
7969
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
@@ -7566,8 +8058,9 @@ function createChart(element, options = {}) {
7566
8058
  if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
7567
8059
  return null;
7568
8060
  }
8061
+ const dividerTolerance = touchInputActive ? 4 * Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 4;
7569
8062
  for (const pane of paneLayoutInfos) {
7570
- if (pane.visible && Math.abs(y - pane.top) <= 4) {
8063
+ if (pane.visible && Math.abs(y - pane.top) <= dividerTolerance) {
7571
8064
  return { id: pane.id, heightPx: pane.bottom - pane.top };
7572
8065
  }
7573
8066
  }
@@ -8060,7 +8553,43 @@ function createChart(element, options = {}) {
8060
8553
  let lastPointerX = 0;
8061
8554
  let lastPointerY = 0;
8062
8555
  let activePointerId = null;
8556
+ let touchInputActive = false;
8063
8557
  const touchPointers = /* @__PURE__ */ new Map();
8558
+ let longPressTimerId = null;
8559
+ let longPressState = null;
8560
+ const cancelLongPressTimer = () => {
8561
+ if (longPressTimerId !== null) {
8562
+ clearTimeout(longPressTimerId);
8563
+ longPressTimerId = null;
8564
+ }
8565
+ };
8566
+ const emitLongPress = (x, y, scrubbing) => {
8567
+ if (!longPressHandler) return;
8568
+ const rect = canvas.getBoundingClientRect();
8569
+ const index = indexFromCanvasX(x);
8570
+ const barTime = index === null ? null : getTimeForIndex(index);
8571
+ const bar = index === null ? void 0 : data[index];
8572
+ longPressHandler({
8573
+ x,
8574
+ y,
8575
+ clientX: rect.left + x,
8576
+ clientY: rect.top + y,
8577
+ scrubbing,
8578
+ ...getHitRegion(x, y) === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
8579
+ ...index === null ? {} : { index },
8580
+ ...barTime ? { time: barTime.toISOString() } : {},
8581
+ ...bar ? {
8582
+ point: {
8583
+ t: bar.time.toISOString(),
8584
+ o: bar.o,
8585
+ h: bar.h,
8586
+ l: bar.l,
8587
+ c: bar.c,
8588
+ ...bar.v === void 0 ? {} : { v: bar.v }
8589
+ }
8590
+ } : {}
8591
+ });
8592
+ };
8064
8593
  let pinchZoomState = null;
8065
8594
  const getTouchPair = () => {
8066
8595
  const points = Array.from(touchPointers.values());
@@ -8126,7 +8655,8 @@ function createChart(element, options = {}) {
8126
8655
  }
8127
8656
  };
8128
8657
  const onPointerDown = (event) => {
8129
- if (event.pointerType === "touch" || event.pointerType === "pen") {
8658
+ touchInputActive = event.pointerType === "touch" || event.pointerType === "pen";
8659
+ if (touchInputActive) {
8130
8660
  event.preventDefault();
8131
8661
  }
8132
8662
  if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
@@ -8143,9 +8673,25 @@ function createChart(element, options = {}) {
8143
8673
  capturePointer(event.pointerId);
8144
8674
  const touchPair = getTouchPair();
8145
8675
  if (touchPair) {
8676
+ cancelLongPressTimer();
8677
+ longPressState = null;
8146
8678
  beginPinchZoom(touchPair[0], touchPair[1]);
8147
8679
  return;
8148
8680
  }
8681
+ cancelLongPressTimer();
8682
+ longPressState = null;
8683
+ const touchOptions = mergedOptions.touch ?? {};
8684
+ const wantsTooltip = touchOptions.longPressTooltip !== false || longPressHandler !== null;
8685
+ if (wantsTooltip && !activeDrawingTool && !draftDrawing && getHitRegion(point.x, point.y) === "plot") {
8686
+ const pointerId = event.pointerId;
8687
+ longPressTimerId = window.setTimeout(() => {
8688
+ longPressTimerId = null;
8689
+ longPressState = { pointerId, x: point.x, y: point.y };
8690
+ setCrosshairPoint(point);
8691
+ scheduleDraw({ overlayOnly: true });
8692
+ emitLongPress(point.x, point.y, false);
8693
+ }, Math.max(120, touchOptions.longPressMs ?? 400));
8694
+ }
8149
8695
  }
8150
8696
  if (measureState) {
8151
8697
  if (measureState.dragging) {
@@ -8311,7 +8857,10 @@ function createChart(element, options = {}) {
8311
8857
  };
8312
8858
  const onPointerMove = (event) => {
8313
8859
  if (event.pointerType === "touch" || event.pointerType === "pen") {
8860
+ touchInputActive = true;
8314
8861
  event.preventDefault();
8862
+ } else if (event.pointerType === "mouse") {
8863
+ touchInputActive = false;
8315
8864
  }
8316
8865
  magnetModifierActive = event.metaKey || event.ctrlKey;
8317
8866
  shiftKeyActive = event.shiftKey;
@@ -8320,6 +8869,8 @@ function createChart(element, options = {}) {
8320
8869
  touchPointers.set(event.pointerId, point);
8321
8870
  const touchPair = getTouchPair();
8322
8871
  if (touchPair) {
8872
+ cancelLongPressTimer();
8873
+ longPressState = null;
8323
8874
  if (!pinchZoomState) {
8324
8875
  beginPinchZoom(touchPair[0], touchPair[1]);
8325
8876
  }
@@ -8329,6 +8880,21 @@ function createChart(element, options = {}) {
8329
8880
  if (pinchZoomState) {
8330
8881
  return;
8331
8882
  }
8883
+ if (longPressState && longPressState.pointerId === event.pointerId) {
8884
+ longPressState = { ...longPressState, x: point.x, y: point.y };
8885
+ setCrosshairPoint(point);
8886
+ emitCrosshairMove(point.x, point.y, getHitRegion(point.x, point.y) === "plot" ? "plot" : "plot");
8887
+ scheduleDraw({ overlayOnly: true });
8888
+ emitLongPress(point.x, point.y, true);
8889
+ return;
8890
+ }
8891
+ if (longPressTimerId !== null) {
8892
+ const dx = point.x - (pointerDownInfo?.x ?? point.x);
8893
+ const dy = point.y - (pointerDownInfo?.y ?? point.y);
8894
+ if (dx * dx + dy * dy > 36) {
8895
+ cancelLongPressTimer();
8896
+ }
8897
+ }
8332
8898
  }
8333
8899
  if (pointerDownInfo && pointerDownInfo.pointerId === event.pointerId && !pointerDownInfo.moved) {
8334
8900
  const dx = point.x - pointerDownInfo.x;
@@ -8592,6 +9158,18 @@ function createChart(element, options = {}) {
8592
9158
  lastPointerY = point.y;
8593
9159
  };
8594
9160
  const endPointerDrag = (event) => {
9161
+ cancelLongPressTimer();
9162
+ if (longPressState && (!event || event.pointerId === longPressState.pointerId)) {
9163
+ longPressState = null;
9164
+ touchPointers.delete(event?.pointerId ?? -1);
9165
+ setCrosshairPoint(null);
9166
+ isDragging = false;
9167
+ dragMode = null;
9168
+ activePointerId = null;
9169
+ pointerDownInfo = null;
9170
+ scheduleDraw({ overlayOnly: true });
9171
+ return;
9172
+ }
8595
9173
  if (event?.pointerType === "touch") {
8596
9174
  touchPointers.delete(event.pointerId);
8597
9175
  if (pinchZoomState) {
@@ -9408,6 +9986,9 @@ function createChart(element, options = {}) {
9408
9986
  const onKeyboardShortcut = (handler) => {
9409
9987
  keyboardShortcutHandler = handler;
9410
9988
  };
9989
+ const onLongPress = (handler) => {
9990
+ longPressHandler = handler;
9991
+ };
9411
9992
  const focus = () => {
9412
9993
  canvas.focus({ preventScroll: true });
9413
9994
  };
@@ -9461,6 +10042,10 @@ function createChart(element, options = {}) {
9461
10042
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
9462
10043
  };
9463
10044
  const destroy = () => {
10045
+ cancelLongPressTimer();
10046
+ datafeedUnsubscribe?.();
10047
+ datafeedUnsubscribe = null;
10048
+ datafeed = null;
9464
10049
  cancelKineticPan();
9465
10050
  if (smoothingRafId !== null) {
9466
10051
  cancelAnimationFrame(smoothingRafId);
@@ -9548,6 +10133,12 @@ function createChart(element, options = {}) {
9548
10133
  getMagnetMode,
9549
10134
  setPriceScale,
9550
10135
  getPriceScale,
10136
+ setCompareSeries,
10137
+ addCompareSeries,
10138
+ removeCompareSeries,
10139
+ getCompareSeries,
10140
+ setDatafeed,
10141
+ onLongPress,
9551
10142
  cancelDrawing,
9552
10143
  setTradeMarkers,
9553
10144
  setSelectedDrawing,