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.
@@ -2853,6 +2853,9 @@ var DEFAULT_OPTIONS = {
2853
2853
  doubleClickAction: "reset",
2854
2854
  keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2855
2855
  accessibility: { label: "Price chart", description: "", focusable: true },
2856
+ downsampling: { enabled: true, thresholdPx: 1.5 },
2857
+ touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
2858
+ datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
2856
2859
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2857
2860
  grid: DEFAULT_GRID_OPTIONS,
2858
2861
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2903,6 +2906,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2903
2906
  ...baseOptions.accessibility,
2904
2907
  ...options.accessibility ?? {}
2905
2908
  },
2909
+ downsampling: {
2910
+ ...baseOptions.downsampling,
2911
+ ...options.downsampling ?? {}
2912
+ },
2913
+ touch: {
2914
+ ...baseOptions.touch,
2915
+ ...options.touch ?? {}
2916
+ },
2917
+ datafeedOptions: {
2918
+ ...baseOptions.datafeedOptions,
2919
+ ...options.datafeedOptions ?? {}
2920
+ },
2906
2921
  crosshair: {
2907
2922
  ...baseOptions.crosshair,
2908
2923
  ...options.crosshair ?? {}
@@ -3077,6 +3092,7 @@ function createChart(element, options = {}) {
3077
3092
  let drawingDoubleClickHandler = null;
3078
3093
  let drawingEditTextHandler = null;
3079
3094
  let selectionChangeHandler = null;
3095
+ let longPressHandler = null;
3080
3096
  let contextMenuHandler = null;
3081
3097
  let keyboardShortcutHandler = null;
3082
3098
  let lastSelectionSignature = null;
@@ -3778,6 +3794,150 @@ function createChart(element, options = {}) {
3778
3794
  heikinAshiFingerprint = fingerprint;
3779
3795
  return result;
3780
3796
  };
3797
+ let compareSeriesStates = [];
3798
+ const prepareCompareSeries = (options2) => {
3799
+ const rows = [];
3800
+ for (const point of options2.data ?? []) {
3801
+ const t = new Date(point.t).getTime();
3802
+ const c = Number(point.c);
3803
+ if (Number.isFinite(t) && Number.isFinite(c)) rows.push({ t, c });
3804
+ }
3805
+ rows.sort((a, b) => a.t - b.t);
3806
+ return {
3807
+ options: options2,
3808
+ times: rows.map((row) => row.t),
3809
+ closes: rows.map((row) => row.c),
3810
+ aligned: null,
3811
+ alignedFingerprint: ""
3812
+ };
3813
+ };
3814
+ const mainDataFingerprint = () => {
3815
+ const last = data[data.length - 1];
3816
+ return last ? `${data.length}|${data[0].time.getTime()}|${last.time.getTime()}` : "empty";
3817
+ };
3818
+ const getAlignedCompareValues = (state) => {
3819
+ const fingerprint = `${mainDataFingerprint()}|${state.times.length}|${state.times[state.times.length - 1] ?? 0}`;
3820
+ if (state.aligned && state.alignedFingerprint === fingerprint) {
3821
+ return state.aligned;
3822
+ }
3823
+ const aligned = new Float64Array(data.length).fill(Number.NaN);
3824
+ let cursor = 0;
3825
+ let lastValue = Number.NaN;
3826
+ for (let i = 0; i < data.length; i += 1) {
3827
+ const barTime = data[i].time.getTime();
3828
+ while (cursor < state.times.length && state.times[cursor] <= barTime) {
3829
+ lastValue = state.closes[cursor];
3830
+ cursor += 1;
3831
+ }
3832
+ aligned[i] = lastValue;
3833
+ }
3834
+ state.aligned = aligned;
3835
+ state.alignedFingerprint = fingerprint;
3836
+ return aligned;
3837
+ };
3838
+ const visibleCompareSeries = () => compareSeriesStates.filter((state) => state.options.visible !== false && state.times.length > 0);
3839
+ const setCompareSeries = (series) => {
3840
+ compareSeriesStates = (series ?? []).filter((entry) => entry && entry.id).map(prepareCompareSeries);
3841
+ scheduleDraw({ updateAutoScale: true });
3842
+ };
3843
+ const addCompareSeries = (series) => {
3844
+ if (!series?.id) return;
3845
+ compareSeriesStates = [...compareSeriesStates.filter((state) => state.options.id !== series.id), prepareCompareSeries(series)];
3846
+ scheduleDraw({ updateAutoScale: true });
3847
+ };
3848
+ const removeCompareSeries = (id) => {
3849
+ const next = compareSeriesStates.filter((state) => state.options.id !== id);
3850
+ if (next.length === compareSeriesStates.length) return;
3851
+ compareSeriesStates = next;
3852
+ scheduleDraw({ updateAutoScale: true });
3853
+ };
3854
+ const getCompareSeries = () => compareSeriesStates.map((state) => state.options);
3855
+ let datafeed = null;
3856
+ let datafeedUnsubscribe = null;
3857
+ let datafeedLoading = false;
3858
+ let datafeedExhausted = false;
3859
+ let datafeedLastRequestAt = 0;
3860
+ const estimateBarStepMs = () => {
3861
+ if (mergedOptions.timeStepMs > 0) return mergedOptions.timeStepMs;
3862
+ if (data.length < 2) return 6e4;
3863
+ const span = data[data.length - 1].time.getTime() - data[0].time.getTime();
3864
+ return Math.max(1e3, span / (data.length - 1));
3865
+ };
3866
+ const requestDatafeedBars = async (request) => {
3867
+ if (!datafeed) return [];
3868
+ try {
3869
+ return await datafeed.getBars(request) ?? [];
3870
+ } catch (error) {
3871
+ datafeed.onError?.(error, request);
3872
+ return [];
3873
+ }
3874
+ };
3875
+ const maybeLoadOlderHistory = () => {
3876
+ if (!datafeed || datafeedLoading || datafeedExhausted || data.length === 0) return;
3877
+ const options2 = mergedOptions.datafeedOptions ?? DEFAULT_OPTIONS.datafeedOptions;
3878
+ const threshold = options2?.prefetchThresholdBars ?? 150;
3879
+ const cooldown = options2?.cooldownMs ?? 1200;
3880
+ if (Date.now() - datafeedLastRequestAt < cooldown) return;
3881
+ if (xCenter - xSpan / 2 > threshold) return;
3882
+ const stepMs = estimateBarStepMs();
3883
+ const chunk = Math.max(10, options2?.chunkBars ?? 1500);
3884
+ const toMs = data[0].time.getTime();
3885
+ datafeedLoading = true;
3886
+ datafeedLastRequestAt = Date.now();
3887
+ void requestDatafeedBars({
3888
+ fromMs: toMs - chunk * stepMs,
3889
+ toMs,
3890
+ countBack: chunk,
3891
+ firstRequest: false
3892
+ }).then((bars) => {
3893
+ const older = bars.filter((bar) => new Date(bar.t).getTime() < toMs);
3894
+ if (older.length === 0) {
3895
+ datafeedExhausted = true;
3896
+ return;
3897
+ }
3898
+ const merged = [...older, ...data.map((bar) => ({
3899
+ t: bar.time.toISOString(),
3900
+ o: bar.o,
3901
+ h: bar.h,
3902
+ l: bar.l,
3903
+ c: bar.c,
3904
+ ...bar.v === void 0 ? {} : { v: bar.v }
3905
+ }))];
3906
+ setData(merged);
3907
+ }).finally(() => {
3908
+ datafeedLoading = false;
3909
+ });
3910
+ };
3911
+ const setDatafeed = async (nextDatafeed) => {
3912
+ datafeedUnsubscribe?.();
3913
+ datafeedUnsubscribe = null;
3914
+ datafeed = nextDatafeed;
3915
+ datafeedExhausted = false;
3916
+ datafeedLoading = false;
3917
+ datafeedLastRequestAt = 0;
3918
+ if (!nextDatafeed) return;
3919
+ await nextDatafeed.onReady?.();
3920
+ if (datafeed !== nextDatafeed) return;
3921
+ const stepMs = estimateBarStepMs();
3922
+ const countBack = Math.max(50, Math.round(mergedOptions.initialVisibleBars * 3));
3923
+ const toMs = Date.now();
3924
+ datafeedLoading = true;
3925
+ const bars = await requestDatafeedBars({
3926
+ fromMs: toMs - countBack * stepMs,
3927
+ toMs,
3928
+ countBack,
3929
+ firstRequest: true
3930
+ });
3931
+ datafeedLoading = false;
3932
+ if (datafeed !== nextDatafeed) return;
3933
+ if (bars.length > 0) {
3934
+ setData(bars);
3935
+ }
3936
+ const unsubscribe = nextDatafeed.subscribeBars?.((bar) => {
3937
+ if (datafeed === nextDatafeed) upsertBar(bar);
3938
+ });
3939
+ datafeedUnsubscribe = typeof unsubscribe === "function" ? unsubscribe : null;
3940
+ };
3781
3941
  const formatHoverTimeLabel = (time, mode) => {
3782
3942
  if (mode === "time") {
3783
3943
  return time.toLocaleTimeString(void 0, {
@@ -4383,6 +4543,31 @@ function createChart(element, options = {}) {
4383
4543
  const chartType = mergedOptions.chartType;
4384
4544
  const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
4385
4545
  const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
4546
+ const comparePlans = [];
4547
+ for (const state of visibleCompareSeries()) {
4548
+ const aligned = getAlignedCompareValues(state);
4549
+ let anchorIndex = -1;
4550
+ for (let index = startIndex; index <= endIndex; index += 1) {
4551
+ if (Number.isFinite(aligned[index] ?? Number.NaN)) {
4552
+ anchorIndex = index;
4553
+ break;
4554
+ }
4555
+ }
4556
+ if (anchorIndex < 0) continue;
4557
+ const baseCompare = aligned[anchorIndex];
4558
+ const baseMain = seriesData[anchorIndex]?.c ?? 0;
4559
+ const usePercent = (state.options.scale ?? "percent") !== "price";
4560
+ const toPrice = usePercent && baseCompare > 0 ? (value) => baseMain * value / baseCompare : (value) => value;
4561
+ let lastNativeValue = Number.NaN;
4562
+ for (let index = endIndex; index >= startIndex; index -= 1) {
4563
+ const value = aligned[index];
4564
+ if (value !== void 0 && Number.isFinite(value)) {
4565
+ lastNativeValue = value;
4566
+ break;
4567
+ }
4568
+ }
4569
+ comparePlans.push({ state, aligned, toPrice, usePercent, baseCompare, lastNativeValue });
4570
+ }
4386
4571
  const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
4387
4572
  const scanCandleRange = (from, to) => {
4388
4573
  let min = Number.POSITIVE_INFINITY;
@@ -4428,6 +4613,23 @@ function createChart(element, options = {}) {
4428
4613
  maxPrice = Math.max(maxPrice, weightedMax);
4429
4614
  }
4430
4615
  }
4616
+ for (const plan of comparePlans) {
4617
+ if (plan.state.options.includeInAutoScale === false) continue;
4618
+ let compareMin = Number.POSITIVE_INFINITY;
4619
+ let compareMax = Number.NEGATIVE_INFINITY;
4620
+ for (let index = startIndex; index <= endIndex; index += 1) {
4621
+ const value = plan.aligned[index];
4622
+ if (value === void 0 || !Number.isFinite(value)) continue;
4623
+ const price = plan.toPrice(value);
4624
+ if (!Number.isFinite(price)) continue;
4625
+ if (price < compareMin) compareMin = price;
4626
+ if (price > compareMax) compareMax = price;
4627
+ }
4628
+ if (compareMin <= compareMax) {
4629
+ minPrice = Math.min(minPrice, compareMin);
4630
+ maxPrice = Math.max(maxPrice, compareMax);
4631
+ }
4632
+ }
4431
4633
  const priceRange = maxPrice - minPrice || 1;
4432
4634
  const autoMin = minPrice - priceRange * 0.08;
4433
4635
  const autoMax = maxPrice + priceRange * 0.08;
@@ -5539,7 +5741,106 @@ function createChart(element, options = {}) {
5539
5741
  }
5540
5742
  return data[index]?.v;
5541
5743
  };
5542
- if (isLineStyleChart) {
5744
+ const downsamplingOptions = mergedOptions.downsampling ?? DEFAULT_OPTIONS.downsampling;
5745
+ const downsampleThresholdPx = Math.max(0.2, downsamplingOptions?.thresholdPx ?? 1.5);
5746
+ const shouldDownsample = (downsamplingOptions?.enabled ?? true) && candleSpacing < downsampleThresholdPx && endIndex - startIndex > 2;
5747
+ const seriesColumns = [];
5748
+ if (shouldDownsample) {
5749
+ let current = null;
5750
+ let currentColumn = Number.NaN;
5751
+ for (let index = startIndex; index <= endIndex; index += 1) {
5752
+ const point = seriesData[index];
5753
+ if (!point) continue;
5754
+ const column = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
5755
+ if (!current || column !== currentColumn) {
5756
+ currentColumn = column;
5757
+ current = {
5758
+ x: column,
5759
+ open: point.o,
5760
+ high: point.h,
5761
+ low: point.l,
5762
+ close: point.c,
5763
+ minClose: point.c,
5764
+ maxClose: point.c,
5765
+ minCloseIndex: index,
5766
+ maxCloseIndex: index
5767
+ };
5768
+ seriesColumns.push(current);
5769
+ continue;
5770
+ }
5771
+ if (point.h > current.high) current.high = point.h;
5772
+ if (point.l < current.low) current.low = point.l;
5773
+ current.close = point.c;
5774
+ if (point.c < current.minClose) {
5775
+ current.minClose = point.c;
5776
+ current.minCloseIndex = index;
5777
+ }
5778
+ if (point.c > current.maxClose) {
5779
+ current.maxClose = point.c;
5780
+ current.maxCloseIndex = index;
5781
+ }
5782
+ }
5783
+ }
5784
+ if (shouldDownsample && !isLineStyleChart) {
5785
+ const upColumns = new Path2D();
5786
+ const downColumns = new Path2D();
5787
+ for (const column of seriesColumns) {
5788
+ const path = column.close >= column.open ? upColumns : downColumns;
5789
+ const x = column.x + 0.5;
5790
+ const top = crisp(yFromPrice(column.high));
5791
+ const bottom = crisp(yFromPrice(column.low));
5792
+ path.moveTo(x, top);
5793
+ path.lineTo(x, bottom === top ? bottom + 1 : bottom);
5794
+ }
5795
+ ctx.lineWidth = 1;
5796
+ ctx.strokeStyle = mergedOptions.upColor;
5797
+ ctx.stroke(upColumns);
5798
+ ctx.strokeStyle = mergedOptions.downColor;
5799
+ ctx.stroke(downColumns);
5800
+ } else if (shouldDownsample && isLineStyleChart) {
5801
+ const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5802
+ const linePath = new Path2D();
5803
+ let started = false;
5804
+ let firstX = 0;
5805
+ let lastX = 0;
5806
+ for (const column of seriesColumns) {
5807
+ const lowFirst = column.minCloseIndex <= column.maxCloseIndex;
5808
+ const first = lowFirst ? column.minClose : column.maxClose;
5809
+ const second = lowFirst ? column.maxClose : column.minClose;
5810
+ const x = column.x;
5811
+ if (!started) {
5812
+ linePath.moveTo(x, yFromPrice(first));
5813
+ firstX = x;
5814
+ started = true;
5815
+ } else {
5816
+ linePath.lineTo(x, yFromPrice(first));
5817
+ }
5818
+ if (second !== first) {
5819
+ linePath.lineTo(x, yFromPrice(second));
5820
+ }
5821
+ lastX = x;
5822
+ }
5823
+ if (started) {
5824
+ if (chartType === "area" || chartType === "baseline") {
5825
+ const baseY = chartType === "baseline" ? yFromPrice(mergedOptions.baselinePrice ?? (yMin + yMax) / 2) : chartBottom;
5826
+ const fillPath = new Path2D(linePath);
5827
+ fillPath.lineTo(lastX, baseY);
5828
+ fillPath.lineTo(firstX, baseY);
5829
+ fillPath.closePath();
5830
+ ctx.save();
5831
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
5832
+ ctx.fillStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5833
+ ctx.fill(fillPath);
5834
+ ctx.restore();
5835
+ }
5836
+ ctx.save();
5837
+ ctx.strokeStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5838
+ ctx.lineWidth = seriesLineWidth;
5839
+ ctx.lineJoin = "round";
5840
+ ctx.stroke(linePath);
5841
+ ctx.restore();
5842
+ }
5843
+ } else if (isLineStyleChart) {
5543
5844
  const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5544
5845
  const linePath = new Path2D();
5545
5846
  let started = false;
@@ -5705,6 +6006,105 @@ function createChart(element, options = {}) {
5705
6006
  ctx.stroke(downHollowBodies);
5706
6007
  }
5707
6008
  }
6009
+ const compareAxisTags = [];
6010
+ for (const plan of comparePlans) {
6011
+ const color = plan.state.options.color ?? "#f59e0b";
6012
+ const lineWidth = Math.max(1, plan.state.options.lineWidth ?? 1.5);
6013
+ const path = new Path2D();
6014
+ let started = false;
6015
+ let firstX = 0;
6016
+ let lastX = 0;
6017
+ let lastY = 0;
6018
+ let lastPrice = Number.NaN;
6019
+ const emit = (x, y) => {
6020
+ if (!started) {
6021
+ path.moveTo(x, y);
6022
+ firstX = x;
6023
+ started = true;
6024
+ } else {
6025
+ path.lineTo(x, y);
6026
+ }
6027
+ lastX = x;
6028
+ lastY = y;
6029
+ };
6030
+ const xForIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
6031
+ if (shouldDownsample) {
6032
+ let column = Number.NaN;
6033
+ let minValue = 0;
6034
+ let maxValue = 0;
6035
+ let minIndex = 0;
6036
+ let maxIndex = 0;
6037
+ let hasColumn = false;
6038
+ const flushColumn = () => {
6039
+ if (!hasColumn) return;
6040
+ const lowFirst = minIndex <= maxIndex;
6041
+ const first = plan.toPrice(lowFirst ? minValue : maxValue);
6042
+ const second = plan.toPrice(lowFirst ? maxValue : minValue);
6043
+ if (Number.isFinite(first)) emit(column, yFromPrice(first));
6044
+ if (Number.isFinite(second) && second !== first) emit(column, yFromPrice(second));
6045
+ lastPrice = plan.toPrice(maxIndex >= minIndex ? maxValue : minValue);
6046
+ };
6047
+ for (let index = startIndex; index <= endIndex; index += 1) {
6048
+ const value = plan.aligned[index];
6049
+ if (value === void 0 || !Number.isFinite(value)) continue;
6050
+ const nextColumn = Math.round(xForIndex(index));
6051
+ if (!hasColumn || nextColumn !== column) {
6052
+ flushColumn();
6053
+ column = nextColumn;
6054
+ minValue = value;
6055
+ maxValue = value;
6056
+ minIndex = index;
6057
+ maxIndex = index;
6058
+ hasColumn = true;
6059
+ continue;
6060
+ }
6061
+ if (value < minValue) {
6062
+ minValue = value;
6063
+ minIndex = index;
6064
+ }
6065
+ if (value > maxValue) {
6066
+ maxValue = value;
6067
+ maxIndex = index;
6068
+ }
6069
+ }
6070
+ flushColumn();
6071
+ } else {
6072
+ for (let index = startIndex; index <= endIndex; index += 1) {
6073
+ const value = plan.aligned[index];
6074
+ if (value === void 0 || !Number.isFinite(value)) continue;
6075
+ const price = plan.toPrice(value);
6076
+ if (!Number.isFinite(price)) continue;
6077
+ lastPrice = price;
6078
+ emit(xForIndex(index), yFromPrice(price));
6079
+ }
6080
+ }
6081
+ if (!started) continue;
6082
+ if (plan.state.options.style === "area") {
6083
+ const fillPath = new Path2D(path);
6084
+ fillPath.lineTo(lastX, chartBottom);
6085
+ fillPath.lineTo(firstX, chartBottom);
6086
+ fillPath.closePath();
6087
+ ctx.save();
6088
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity)) * 0.6;
6089
+ ctx.fillStyle = color;
6090
+ ctx.fill(fillPath);
6091
+ ctx.restore();
6092
+ }
6093
+ ctx.save();
6094
+ ctx.strokeStyle = color;
6095
+ ctx.lineWidth = lineWidth;
6096
+ ctx.lineJoin = "round";
6097
+ ctx.stroke(path);
6098
+ ctx.restore();
6099
+ if (Number.isFinite(lastPrice)) {
6100
+ const label = plan.state.options.label ?? plan.state.options.id;
6101
+ const nativeLast = plan.lastNativeValue;
6102
+ const usePercentTag = plan.usePercent && plan.baseCompare > 0 && Number.isFinite(nativeLast);
6103
+ const changePct = usePercentTag ? (nativeLast / plan.baseCompare - 1) * 100 : 0;
6104
+ const shortText = usePercentTag ? `${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%` : formatPrice(nativeLast);
6105
+ compareAxisTags.push({ y: lastY, text: `${label} ${shortText}`, shortText, color });
6106
+ }
6107
+ }
5708
6108
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
5709
6109
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
5710
6110
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
@@ -6479,6 +6879,35 @@ function createChart(element, options = {}) {
6479
6879
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
6480
6880
  ctx.font = prevFont;
6481
6881
  }
6882
+ if (compareAxisTags.length > 0 && width - chartRight > 0) {
6883
+ const prevFont = ctx.font;
6884
+ const tagFontSize = Math.max(9, (resolvedAxis.fontSize ?? 12) - 2);
6885
+ ctx.font = `${tagFontSize}px ${mergedOptions.fontFamily}`;
6886
+ ctx.textBaseline = "middle";
6887
+ ctx.textAlign = "left";
6888
+ const placed = [];
6889
+ const tagHeight = tagFontSize + 6;
6890
+ const gutterWidth = width - chartRight - 4;
6891
+ for (const tag of [...compareAxisTags].sort((a, b) => a.y - b.y)) {
6892
+ const text = measureTextWidth(tag.text) + 10 <= gutterWidth ? tag.text : tag.shortText;
6893
+ const tagWidth = Math.min(gutterWidth, measureTextWidth(text) + 10);
6894
+ if (tagWidth <= 0) continue;
6895
+ let tagY = clamp(tag.y - tagHeight / 2, chartTop, chartBottom - tagHeight);
6896
+ for (const slot of placed) {
6897
+ if (tagY < slot.bottom + 2 && tagY + tagHeight > slot.top - 2) {
6898
+ tagY = slot.bottom + 2;
6899
+ }
6900
+ }
6901
+ placed.push({ top: tagY, bottom: tagY + tagHeight });
6902
+ ctx.save();
6903
+ ctx.fillStyle = tag.color;
6904
+ fillRoundedRect(chartRight + 2, Math.round(tagY), Math.round(tagWidth), tagHeight, 3);
6905
+ ctx.fillStyle = labelTextColorOn(tag.color, "#ffffff");
6906
+ ctx.fillText(text, chartRight + 7, Math.round(tagY + tagHeight / 2));
6907
+ ctx.restore();
6908
+ }
6909
+ ctx.font = prevFont;
6910
+ }
6482
6911
  if (baseCtx) {
6483
6912
  if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
6484
6913
  baseCanvas.width = canvas.width;
@@ -6503,6 +6932,7 @@ function createChart(element, options = {}) {
6503
6932
  };
6504
6933
  paintCrosshairOverlay();
6505
6934
  emitSelectionChangeIfMoved();
6935
+ maybeLoadOlderHistory();
6506
6936
  };
6507
6937
  const paintCrosshairOverlay = () => {
6508
6938
  crosshairPriceActionRegion = null;
@@ -6695,6 +7125,67 @@ function createChart(element, options = {}) {
6695
7125
  drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
6696
7126
  }
6697
7127
  }
7128
+ if (longPressState && (mergedOptions.touch?.longPressTooltip ?? true)) {
7129
+ const index = indexFromCanvasX(cx);
7130
+ const bar = index === null ? void 0 : data[index];
7131
+ if (bar) {
7132
+ const barTime = getTimeForIndex(index);
7133
+ const changeAbs = bar.c - bar.o;
7134
+ const changePct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
7135
+ const rows = [
7136
+ ["O", formatPrice(bar.o), null],
7137
+ ["H", formatPrice(bar.h), null],
7138
+ ["L", formatPrice(bar.l), null],
7139
+ ["C", formatPrice(bar.c), null],
7140
+ [
7141
+ "Chg",
7142
+ `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${changePct.toFixed(2)}%)`,
7143
+ changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor
7144
+ ]
7145
+ ];
7146
+ if (bar.v !== void 0) {
7147
+ rows.push([
7148
+ "Vol",
7149
+ bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v)),
7150
+ null
7151
+ ]);
7152
+ }
7153
+ const prevFont = ctx.font;
7154
+ const fontSize = 11;
7155
+ ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
7156
+ const headerText = barTime ? formatHoverTimeLabel(barTime, "auto") : "";
7157
+ const rowGap = 3;
7158
+ const padding = 8;
7159
+ const labelColumn = 26;
7160
+ const contentWidth = Math.max(
7161
+ measureTextWidth(headerText),
7162
+ ...rows.map(([, value]) => labelColumn + measureTextWidth(value))
7163
+ );
7164
+ const boxWidth = Math.ceil(contentWidth) + padding * 2;
7165
+ const boxHeight = padding * 2 + (fontSize + rowGap) * (rows.length + (headerText ? 1 : 0)) - rowGap;
7166
+ const fingerGap = 26;
7167
+ const boxX = cx + fingerGap + boxWidth <= chartRight ? cx + fingerGap : Math.max(chartLeft + 4, cx - fingerGap - boxWidth);
7168
+ const boxY = clamp(cy - boxHeight - fingerGap, chartTop + 4, chartBottom - boxHeight - 4);
7169
+ ctx.save();
7170
+ ctx.fillStyle = "rgba(16,17,20,0.94)";
7171
+ fillRoundedRect(Math.round(boxX), Math.round(boxY), boxWidth, boxHeight, 6);
7172
+ ctx.strokeStyle = "rgba(255,255,255,0.14)";
7173
+ ctx.lineWidth = 1;
7174
+ ctx.strokeRect(Math.round(boxX) + 0.5, Math.round(boxY) + 0.5, boxWidth - 1, boxHeight - 1);
7175
+ let textY = boxY + padding + fontSize / 2;
7176
+ if (headerText) {
7177
+ drawText(headerText, boxX + padding, textY, "left", "middle", "rgba(255,255,255,0.55)");
7178
+ textY += fontSize + rowGap;
7179
+ }
7180
+ for (const [label, value, valueColor] of rows) {
7181
+ drawText(label, boxX + padding, textY, "left", "middle", "rgba(255,255,255,0.5)");
7182
+ drawText(value, boxX + padding + labelColumn, textY, "left", "middle", valueColor ?? "#e6e8ee");
7183
+ textY += fontSize + rowGap;
7184
+ }
7185
+ ctx.restore();
7186
+ ctx.font = prevFont;
7187
+ }
7188
+ }
6698
7189
  };
6699
7190
  const drawOverlayOnly = () => {
6700
7191
  if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
@@ -7204,6 +7695,7 @@ function createChart(element, options = {}) {
7204
7695
  return Math.hypot(x - (x1 + t * dx), y - (y1 + t * dy));
7205
7696
  };
7206
7697
  const getDrawingHit = (x, y) => {
7698
+ const hitTolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
7207
7699
  for (let index = drawings.length - 1; index >= 0; index -= 1) {
7208
7700
  const drawing = drawings[index];
7209
7701
  if (!drawing?.visible) continue;
@@ -7212,10 +7704,10 @@ function createChart(element, options = {}) {
7212
7704
  if (!point) continue;
7213
7705
  const lineY = canvasYFromDrawingPrice(point.price);
7214
7706
  const handleX = drawState ? drawState.chartRight - 64 : 0;
7215
- if (Math.hypot(x - handleX, y - lineY) <= 8) {
7707
+ if (Math.hypot(x - handleX, y - lineY) <= 8 * hitTolerance) {
7216
7708
  return { drawing, target: "handle", pointIndex: 0 };
7217
7709
  }
7218
- if (Math.abs(y - lineY) <= 7) {
7710
+ if (Math.abs(y - lineY) <= 7 * hitTolerance) {
7219
7711
  return { drawing, target: "line" };
7220
7712
  }
7221
7713
  } else if (drawing.type === "vertical-line") {
@@ -7223,10 +7715,10 @@ function createChart(element, options = {}) {
7223
7715
  if (!point) continue;
7224
7716
  const lineX = canvasXFromDrawingPoint(point);
7225
7717
  const handleY = drawState ? (drawState.chartTop + drawState.chartBottom) / 2 : 0;
7226
- if (Math.hypot(x - lineX, y - handleY) <= 8) {
7718
+ if (Math.hypot(x - lineX, y - handleY) <= 8 * hitTolerance) {
7227
7719
  return { drawing, target: "handle", pointIndex: 0 };
7228
7720
  }
7229
- if (Math.abs(x - lineX) <= 7) {
7721
+ if (Math.abs(x - lineX) <= 7 * hitTolerance) {
7230
7722
  return { drawing, target: "line" };
7231
7723
  }
7232
7724
  } else if (drawing.type === "trendline") {
@@ -7237,13 +7729,13 @@ function createChart(element, options = {}) {
7237
7729
  const y1 = canvasYFromDrawingPrice(first.price);
7238
7730
  const x2 = canvasXFromDrawingPoint(second);
7239
7731
  const y2 = canvasYFromDrawingPrice(second.price);
7240
- if (Math.hypot(x - x1, y - y1) <= 8) {
7732
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7241
7733
  return { drawing, target: "handle", pointIndex: 0 };
7242
7734
  }
7243
- if (Math.hypot(x - x2, y - y2) <= 8) {
7735
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7244
7736
  return { drawing, target: "handle", pointIndex: 1 };
7245
7737
  }
7246
- if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7738
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7247
7739
  return { drawing, target: "line" };
7248
7740
  }
7249
7741
  } else if (drawing.type === "ray") {
@@ -7254,10 +7746,10 @@ function createChart(element, options = {}) {
7254
7746
  const y1 = canvasYFromDrawingPrice(first.price);
7255
7747
  const x2 = canvasXFromDrawingPoint(second);
7256
7748
  const y2 = canvasYFromDrawingPrice(second.price);
7257
- if (Math.hypot(x - x1, y - y1) <= 8) {
7749
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7258
7750
  return { drawing, target: "handle", pointIndex: 0 };
7259
7751
  }
7260
- if (Math.hypot(x - x2, y - y2) <= 8) {
7752
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7261
7753
  return { drawing, target: "handle", pointIndex: 1 };
7262
7754
  }
7263
7755
  const dx = x2 - x1;
@@ -7267,7 +7759,7 @@ function createChart(element, options = {}) {
7267
7759
  const t = Math.max(0, ((x - x1) * dx + (y - y1) * dy) / lengthSq);
7268
7760
  const projX = x1 + t * dx;
7269
7761
  const projY = y1 + t * dy;
7270
- if (Math.hypot(x - projX, y - projY) <= 6) {
7762
+ if (Math.hypot(x - projX, y - projY) <= 6 * hitTolerance) {
7271
7763
  return { drawing, target: "line" };
7272
7764
  }
7273
7765
  }
@@ -7279,17 +7771,17 @@ function createChart(element, options = {}) {
7279
7771
  const y1 = canvasYFromDrawingPrice(first.price);
7280
7772
  const x2 = canvasXFromDrawingPoint(second);
7281
7773
  const y2 = canvasYFromDrawingPrice(second.price);
7282
- if (Math.hypot(x - x1, y - y1) <= 9) {
7774
+ if (Math.hypot(x - x1, y - y1) <= 9 * hitTolerance) {
7283
7775
  return { drawing, target: "handle", pointIndex: 0 };
7284
7776
  }
7285
- if (Math.hypot(x - x2, y - y2) <= 9) {
7777
+ if (Math.hypot(x - x2, y - y2) <= 9 * hitTolerance) {
7286
7778
  return { drawing, target: "handle", pointIndex: 1 };
7287
7779
  }
7288
7780
  const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
7289
7781
  for (const ratio of fibRatios) {
7290
7782
  const price = first.price + (second.price - first.price) * (1 - ratio);
7291
7783
  const lineY = canvasYFromDrawingPrice(price);
7292
- if (Math.abs(y - lineY) <= 5) {
7784
+ if (Math.abs(y - lineY) <= 5 * hitTolerance) {
7293
7785
  return { drawing, target: "line" };
7294
7786
  }
7295
7787
  }
@@ -7302,30 +7794,30 @@ function createChart(element, options = {}) {
7302
7794
  const y0 = canvasYFromDrawingPrice(p0.price);
7303
7795
  const x1 = canvasXFromDrawingPoint(p1);
7304
7796
  const y1 = canvasYFromDrawingPrice(p1.price);
7305
- if (Math.hypot(x - x0, y - y0) <= 8) {
7797
+ if (Math.hypot(x - x0, y - y0) <= 8 * hitTolerance) {
7306
7798
  return { drawing, target: "handle", pointIndex: 0 };
7307
7799
  }
7308
- if (Math.hypot(x - x1, y - y1) <= 8) {
7800
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7309
7801
  return { drawing, target: "handle", pointIndex: 1 };
7310
7802
  }
7311
7803
  if (p2) {
7312
7804
  const x2 = canvasXFromDrawingPoint(p2);
7313
7805
  const y2 = canvasYFromDrawingPrice(p2.price);
7314
- if (Math.hypot(x - x2, y - y2) <= 8) {
7806
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7315
7807
  return { drawing, target: "handle", pointIndex: 2 };
7316
7808
  }
7317
7809
  const move = p1.price - p0.price;
7318
7810
  const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
7319
7811
  for (const ratio of fibExtRatios) {
7320
7812
  const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
7321
- if (Math.abs(y - lineY) <= 5) {
7813
+ if (Math.abs(y - lineY) <= 5 * hitTolerance) {
7322
7814
  return { drawing, target: "line" };
7323
7815
  }
7324
7816
  }
7325
- if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7817
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance || distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7326
7818
  return { drawing, target: "line" };
7327
7819
  }
7328
- } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7820
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance) {
7329
7821
  return { drawing, target: "line" };
7330
7822
  }
7331
7823
  } else if (drawing.type === "long-position" || drawing.type === "short-position") {
@@ -7339,10 +7831,10 @@ function createChart(element, options = {}) {
7339
7831
  const entryY = canvasYFromDrawingPrice(entry.price);
7340
7832
  const targetY = canvasYFromDrawingPrice(target.price);
7341
7833
  const stopY = canvasYFromDrawingPrice(stop.price);
7342
- if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7343
- if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7344
- if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7345
- if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7834
+ if (Math.hypot(x - leftX, y - targetY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7835
+ if (Math.hypot(x - leftX, y - entryY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7836
+ if (Math.hypot(x - leftX, y - stopY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7837
+ if (Math.hypot(x - rightX, y - entryY) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7346
7838
  const x0 = Math.min(leftX, rightX);
7347
7839
  const x1 = Math.max(leftX, rightX);
7348
7840
  const yTop = Math.min(targetY, stopY, entryY);
@@ -7358,10 +7850,10 @@ function createChart(element, options = {}) {
7358
7850
  const px1 = canvasXFromDrawingPoint(p1);
7359
7851
  const y0 = canvasYFromDrawingPrice(p0.price);
7360
7852
  const y1 = canvasYFromDrawingPrice(p1.price);
7361
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7362
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7363
- if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7364
- if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7853
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7854
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7855
+ if (Math.hypot(x - px0, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7856
+ if (Math.hypot(x - px1, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7365
7857
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
7366
7858
  return { drawing, target: "line" };
7367
7859
  }
@@ -7373,16 +7865,16 @@ function createChart(element, options = {}) {
7373
7865
  const px1 = canvasXFromDrawingPoint(p1);
7374
7866
  const y0 = canvasYFromDrawingPrice(p0.price);
7375
7867
  const y1 = canvasYFromDrawingPrice(p1.price);
7376
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7377
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7378
- if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7379
- if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7868
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7869
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7870
+ if (Math.hypot(x - px0, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7871
+ if (Math.hypot(x - px1, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 3 };
7380
7872
  const cx = (px0 + px1) / 2;
7381
7873
  const cy = (y0 + y1) / 2;
7382
7874
  const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7383
7875
  const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7384
7876
  const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7385
- if (norm <= 1) {
7877
+ if (norm <= 1 * hitTolerance) {
7386
7878
  return { drawing, target: "line" };
7387
7879
  }
7388
7880
  } else if (drawing.type === "parallel-channel") {
@@ -7394,18 +7886,18 @@ function createChart(element, options = {}) {
7394
7886
  const y0 = canvasYFromDrawingPrice(p0.price);
7395
7887
  const x1 = canvasXFromDrawingPoint(p1);
7396
7888
  const y1 = canvasYFromDrawingPrice(p1.price);
7397
- if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7398
- if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7889
+ if (Math.hypot(x - x0, y - y0) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7890
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7399
7891
  if (p2) {
7400
7892
  const x2 = canvasXFromDrawingPoint(p2);
7401
7893
  const y2 = canvasYFromDrawingPrice(p2.price);
7402
- if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7894
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) return { drawing, target: "handle", pointIndex: 2 };
7403
7895
  const indexSpan = p1.index - p0.index;
7404
7896
  const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7405
7897
  const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7406
7898
  const q0y = canvasYFromDrawingPrice(p0.price + offset);
7407
7899
  const q1y = canvasYFromDrawingPrice(p1.price + offset);
7408
- if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7900
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6 * hitTolerance) {
7409
7901
  return { drawing, target: "line" };
7410
7902
  }
7411
7903
  const minX = Math.min(x0, x1);
@@ -7418,7 +7910,7 @@ function createChart(element, options = {}) {
7418
7910
  return { drawing, target: "line" };
7419
7911
  }
7420
7912
  }
7421
- } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7913
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 * hitTolerance) {
7422
7914
  return { drawing, target: "line" };
7423
7915
  }
7424
7916
  } else if (drawing.type === "arrow") {
@@ -7429,13 +7921,13 @@ function createChart(element, options = {}) {
7429
7921
  const y1 = canvasYFromDrawingPrice(first.price);
7430
7922
  const x2 = canvasXFromDrawingPoint(second);
7431
7923
  const y2 = canvasYFromDrawingPrice(second.price);
7432
- if (Math.hypot(x - x1, y - y1) <= 8) {
7924
+ if (Math.hypot(x - x1, y - y1) <= 8 * hitTolerance) {
7433
7925
  return { drawing, target: "handle", pointIndex: 0 };
7434
7926
  }
7435
- if (Math.hypot(x - x2, y - y2) <= 8) {
7927
+ if (Math.hypot(x - x2, y - y2) <= 8 * hitTolerance) {
7436
7928
  return { drawing, target: "handle", pointIndex: 1 };
7437
7929
  }
7438
- if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7930
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6 * hitTolerance) {
7439
7931
  return { drawing, target: "line" };
7440
7932
  }
7441
7933
  } else if (drawing.type === "brush") {
@@ -7447,7 +7939,7 @@ function createChart(element, options = {}) {
7447
7939
  for (let i = 1; i < pts.length; i += 1) {
7448
7940
  const nextX = canvasXFromDrawingPoint(pts[i]);
7449
7941
  const nextY = canvasYFromDrawingPrice(pts[i].price);
7450
- if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7942
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7 * hitTolerance) {
7451
7943
  hitBody = true;
7452
7944
  break;
7453
7945
  }
@@ -7465,8 +7957,8 @@ function createChart(element, options = {}) {
7465
7957
  const ay = canvasYFromDrawingPrice(anchor.price);
7466
7958
  const bx = canvasXFromDrawingPoint(boxPoint);
7467
7959
  const by = canvasYFromDrawingPrice(boxPoint.price);
7468
- if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7469
- if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7960
+ if (Math.hypot(x - ax, y - ay) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7961
+ if (Math.hypot(x - bx, y - by) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7470
7962
  const fontSize = Math.max(6, drawing.fontSize);
7471
7963
  const prevFont = ctx.font;
7472
7964
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
@@ -7482,7 +7974,7 @@ function createChart(element, options = {}) {
7482
7974
  if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7483
7975
  return { drawing, target: "line" };
7484
7976
  }
7485
- if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7977
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
7486
7978
  return { drawing, target: "line" };
7487
7979
  }
7488
7980
  } else if (drawing.type === "price-range") {
@@ -7493,8 +7985,8 @@ function createChart(element, options = {}) {
7493
7985
  const px1 = canvasXFromDrawingPoint(p1);
7494
7986
  const y0 = canvasYFromDrawingPrice(p0.price);
7495
7987
  const y1 = canvasYFromDrawingPrice(p1.price);
7496
- if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7497
- if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7988
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7989
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
7498
7990
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
7499
7991
  return { drawing, target: "line" };
7500
7992
  }
@@ -7503,7 +7995,7 @@ function createChart(element, options = {}) {
7503
7995
  if (!anchor) continue;
7504
7996
  const ax = canvasXFromDrawingPoint(anchor);
7505
7997
  const ay = canvasYFromDrawingPrice(anchor.price);
7506
- if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7998
+ if (Math.hypot(x - ax, y - ay) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
7507
7999
  const fontSize = Math.max(6, drawing.fontSize);
7508
8000
  const prevFont = ctx.font;
7509
8001
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
@@ -7598,8 +8090,9 @@ function createChart(element, options = {}) {
7598
8090
  if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
7599
8091
  return null;
7600
8092
  }
8093
+ const dividerTolerance = touchInputActive ? 4 * Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 4;
7601
8094
  for (const pane of paneLayoutInfos) {
7602
- if (pane.visible && Math.abs(y - pane.top) <= 4) {
8095
+ if (pane.visible && Math.abs(y - pane.top) <= dividerTolerance) {
7603
8096
  return { id: pane.id, heightPx: pane.bottom - pane.top };
7604
8097
  }
7605
8098
  }
@@ -8092,7 +8585,43 @@ function createChart(element, options = {}) {
8092
8585
  let lastPointerX = 0;
8093
8586
  let lastPointerY = 0;
8094
8587
  let activePointerId = null;
8588
+ let touchInputActive = false;
8095
8589
  const touchPointers = /* @__PURE__ */ new Map();
8590
+ let longPressTimerId = null;
8591
+ let longPressState = null;
8592
+ const cancelLongPressTimer = () => {
8593
+ if (longPressTimerId !== null) {
8594
+ clearTimeout(longPressTimerId);
8595
+ longPressTimerId = null;
8596
+ }
8597
+ };
8598
+ const emitLongPress = (x, y, scrubbing) => {
8599
+ if (!longPressHandler) return;
8600
+ const rect = canvas.getBoundingClientRect();
8601
+ const index = indexFromCanvasX(x);
8602
+ const barTime = index === null ? null : getTimeForIndex(index);
8603
+ const bar = index === null ? void 0 : data[index];
8604
+ longPressHandler({
8605
+ x,
8606
+ y,
8607
+ clientX: rect.left + x,
8608
+ clientY: rect.top + y,
8609
+ scrubbing,
8610
+ ...getHitRegion(x, y) === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
8611
+ ...index === null ? {} : { index },
8612
+ ...barTime ? { time: barTime.toISOString() } : {},
8613
+ ...bar ? {
8614
+ point: {
8615
+ t: bar.time.toISOString(),
8616
+ o: bar.o,
8617
+ h: bar.h,
8618
+ l: bar.l,
8619
+ c: bar.c,
8620
+ ...bar.v === void 0 ? {} : { v: bar.v }
8621
+ }
8622
+ } : {}
8623
+ });
8624
+ };
8096
8625
  let pinchZoomState = null;
8097
8626
  const getTouchPair = () => {
8098
8627
  const points = Array.from(touchPointers.values());
@@ -8158,7 +8687,8 @@ function createChart(element, options = {}) {
8158
8687
  }
8159
8688
  };
8160
8689
  const onPointerDown = (event) => {
8161
- if (event.pointerType === "touch" || event.pointerType === "pen") {
8690
+ touchInputActive = event.pointerType === "touch" || event.pointerType === "pen";
8691
+ if (touchInputActive) {
8162
8692
  event.preventDefault();
8163
8693
  }
8164
8694
  if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
@@ -8175,9 +8705,25 @@ function createChart(element, options = {}) {
8175
8705
  capturePointer(event.pointerId);
8176
8706
  const touchPair = getTouchPair();
8177
8707
  if (touchPair) {
8708
+ cancelLongPressTimer();
8709
+ longPressState = null;
8178
8710
  beginPinchZoom(touchPair[0], touchPair[1]);
8179
8711
  return;
8180
8712
  }
8713
+ cancelLongPressTimer();
8714
+ longPressState = null;
8715
+ const touchOptions = mergedOptions.touch ?? {};
8716
+ const wantsTooltip = touchOptions.longPressTooltip !== false || longPressHandler !== null;
8717
+ if (wantsTooltip && !activeDrawingTool && !draftDrawing && getHitRegion(point.x, point.y) === "plot") {
8718
+ const pointerId = event.pointerId;
8719
+ longPressTimerId = window.setTimeout(() => {
8720
+ longPressTimerId = null;
8721
+ longPressState = { pointerId, x: point.x, y: point.y };
8722
+ setCrosshairPoint(point);
8723
+ scheduleDraw({ overlayOnly: true });
8724
+ emitLongPress(point.x, point.y, false);
8725
+ }, Math.max(120, touchOptions.longPressMs ?? 400));
8726
+ }
8181
8727
  }
8182
8728
  if (measureState) {
8183
8729
  if (measureState.dragging) {
@@ -8343,7 +8889,10 @@ function createChart(element, options = {}) {
8343
8889
  };
8344
8890
  const onPointerMove = (event) => {
8345
8891
  if (event.pointerType === "touch" || event.pointerType === "pen") {
8892
+ touchInputActive = true;
8346
8893
  event.preventDefault();
8894
+ } else if (event.pointerType === "mouse") {
8895
+ touchInputActive = false;
8347
8896
  }
8348
8897
  magnetModifierActive = event.metaKey || event.ctrlKey;
8349
8898
  shiftKeyActive = event.shiftKey;
@@ -8352,6 +8901,8 @@ function createChart(element, options = {}) {
8352
8901
  touchPointers.set(event.pointerId, point);
8353
8902
  const touchPair = getTouchPair();
8354
8903
  if (touchPair) {
8904
+ cancelLongPressTimer();
8905
+ longPressState = null;
8355
8906
  if (!pinchZoomState) {
8356
8907
  beginPinchZoom(touchPair[0], touchPair[1]);
8357
8908
  }
@@ -8361,6 +8912,21 @@ function createChart(element, options = {}) {
8361
8912
  if (pinchZoomState) {
8362
8913
  return;
8363
8914
  }
8915
+ if (longPressState && longPressState.pointerId === event.pointerId) {
8916
+ longPressState = { ...longPressState, x: point.x, y: point.y };
8917
+ setCrosshairPoint(point);
8918
+ emitCrosshairMove(point.x, point.y, getHitRegion(point.x, point.y) === "plot" ? "plot" : "plot");
8919
+ scheduleDraw({ overlayOnly: true });
8920
+ emitLongPress(point.x, point.y, true);
8921
+ return;
8922
+ }
8923
+ if (longPressTimerId !== null) {
8924
+ const dx = point.x - (pointerDownInfo?.x ?? point.x);
8925
+ const dy = point.y - (pointerDownInfo?.y ?? point.y);
8926
+ if (dx * dx + dy * dy > 36) {
8927
+ cancelLongPressTimer();
8928
+ }
8929
+ }
8364
8930
  }
8365
8931
  if (pointerDownInfo && pointerDownInfo.pointerId === event.pointerId && !pointerDownInfo.moved) {
8366
8932
  const dx = point.x - pointerDownInfo.x;
@@ -8624,6 +9190,18 @@ function createChart(element, options = {}) {
8624
9190
  lastPointerY = point.y;
8625
9191
  };
8626
9192
  const endPointerDrag = (event) => {
9193
+ cancelLongPressTimer();
9194
+ if (longPressState && (!event || event.pointerId === longPressState.pointerId)) {
9195
+ longPressState = null;
9196
+ touchPointers.delete(event?.pointerId ?? -1);
9197
+ setCrosshairPoint(null);
9198
+ isDragging = false;
9199
+ dragMode = null;
9200
+ activePointerId = null;
9201
+ pointerDownInfo = null;
9202
+ scheduleDraw({ overlayOnly: true });
9203
+ return;
9204
+ }
8627
9205
  if (event?.pointerType === "touch") {
8628
9206
  touchPointers.delete(event.pointerId);
8629
9207
  if (pinchZoomState) {
@@ -9440,6 +10018,9 @@ function createChart(element, options = {}) {
9440
10018
  const onKeyboardShortcut = (handler) => {
9441
10019
  keyboardShortcutHandler = handler;
9442
10020
  };
10021
+ const onLongPress = (handler) => {
10022
+ longPressHandler = handler;
10023
+ };
9443
10024
  const focus = () => {
9444
10025
  canvas.focus({ preventScroll: true });
9445
10026
  };
@@ -9493,6 +10074,10 @@ function createChart(element, options = {}) {
9493
10074
  return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
9494
10075
  };
9495
10076
  const destroy = () => {
10077
+ cancelLongPressTimer();
10078
+ datafeedUnsubscribe?.();
10079
+ datafeedUnsubscribe = null;
10080
+ datafeed = null;
9496
10081
  cancelKineticPan();
9497
10082
  if (smoothingRafId !== null) {
9498
10083
  cancelAnimationFrame(smoothingRafId);
@@ -9580,6 +10165,12 @@ function createChart(element, options = {}) {
9580
10165
  getMagnetMode,
9581
10166
  setPriceScale,
9582
10167
  getPriceScale,
10168
+ setCompareSeries,
10169
+ addCompareSeries,
10170
+ removeCompareSeries,
10171
+ getCompareSeries,
10172
+ setDatafeed,
10173
+ onLongPress,
9583
10174
  cancelDrawing,
9584
10175
  setTradeMarkers,
9585
10176
  setSelectedDrawing,