hyperprop-charting-library 0.1.137 → 0.1.139

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.
@@ -2819,6 +2819,9 @@ var DEFAULT_OPTIONS = {
2819
2819
  pinOutOfRangeLines: false,
2820
2820
  doubleClickEnabled: true,
2821
2821
  doubleClickAction: "reset",
2822
+ keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2823
+ accessibility: { label: "Price chart", description: "", focusable: true },
2824
+ downsampling: { enabled: true, thresholdPx: 1.5 },
2822
2825
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2823
2826
  grid: DEFAULT_GRID_OPTIONS,
2824
2827
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2861,6 +2864,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2861
2864
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
2862
2865
  ...options.yAxis ?? {}
2863
2866
  },
2867
+ keyboard: {
2868
+ ...baseOptions.keyboard,
2869
+ ...options.keyboard ?? {}
2870
+ },
2871
+ accessibility: {
2872
+ ...baseOptions.accessibility,
2873
+ ...options.accessibility ?? {}
2874
+ },
2875
+ downsampling: {
2876
+ ...baseOptions.downsampling,
2877
+ ...options.downsampling ?? {}
2878
+ },
2864
2879
  crosshair: {
2865
2880
  ...baseOptions.crosshair,
2866
2881
  ...options.crosshair ?? {}
@@ -3034,6 +3049,10 @@ function createChart(element, options = {}) {
3034
3049
  let drawingSelectHandler = null;
3035
3050
  let drawingDoubleClickHandler = null;
3036
3051
  let drawingEditTextHandler = null;
3052
+ let selectionChangeHandler = null;
3053
+ let contextMenuHandler = null;
3054
+ let keyboardShortcutHandler = null;
3055
+ let lastSelectionSignature = null;
3037
3056
  let magnetMode = "none";
3038
3057
  let priceScaleMode = "linear";
3039
3058
  let priceScaleInverted = false;
@@ -3196,6 +3215,39 @@ function createChart(element, options = {}) {
3196
3215
  canvas.style.setProperty("-webkit-user-select", "none");
3197
3216
  canvas.style.setProperty("-webkit-touch-callout", "none");
3198
3217
  canvas.setAttribute("draggable", "false");
3218
+ const applyAccessibilityAttributes = () => {
3219
+ const a11y = mergedOptions.accessibility ?? {};
3220
+ const focusable = a11y.focusable !== false;
3221
+ if (focusable) {
3222
+ canvas.tabIndex = 0;
3223
+ } else {
3224
+ canvas.removeAttribute("tabindex");
3225
+ }
3226
+ canvas.setAttribute("role", "application");
3227
+ canvas.setAttribute("aria-label", a11y.label ?? "Price chart");
3228
+ if (a11y.description) {
3229
+ canvas.setAttribute("aria-description", a11y.description);
3230
+ } else {
3231
+ canvas.removeAttribute("aria-description");
3232
+ }
3233
+ if (focusable && (mergedOptions.keyboard?.enabled ?? true)) {
3234
+ canvas.setAttribute(
3235
+ "aria-keyshortcuts",
3236
+ "ArrowLeft ArrowRight ArrowUp ArrowDown Plus Minus Home End Delete Escape"
3237
+ );
3238
+ } else {
3239
+ canvas.removeAttribute("aria-keyshortcuts");
3240
+ }
3241
+ };
3242
+ applyAccessibilityAttributes();
3243
+ let focusFromPointer = false;
3244
+ canvas.addEventListener("focus", () => {
3245
+ canvas.style.outline = focusFromPointer ? "none" : "";
3246
+ focusFromPointer = false;
3247
+ });
3248
+ canvas.addEventListener("blur", () => {
3249
+ canvas.style.outline = "none";
3250
+ });
3199
3251
  element.innerHTML = "";
3200
3252
  element.appendChild(canvas);
3201
3253
  const baseCanvas = document.createElement("canvas");
@@ -3699,6 +3751,64 @@ function createChart(element, options = {}) {
3699
3751
  heikinAshiFingerprint = fingerprint;
3700
3752
  return result;
3701
3753
  };
3754
+ let compareSeriesStates = [];
3755
+ const prepareCompareSeries = (options2) => {
3756
+ const rows = [];
3757
+ for (const point of options2.data ?? []) {
3758
+ const t = new Date(point.t).getTime();
3759
+ const c = Number(point.c);
3760
+ if (Number.isFinite(t) && Number.isFinite(c)) rows.push({ t, c });
3761
+ }
3762
+ rows.sort((a, b) => a.t - b.t);
3763
+ return {
3764
+ options: options2,
3765
+ times: rows.map((row) => row.t),
3766
+ closes: rows.map((row) => row.c),
3767
+ aligned: null,
3768
+ alignedFingerprint: ""
3769
+ };
3770
+ };
3771
+ const mainDataFingerprint = () => {
3772
+ const last = data[data.length - 1];
3773
+ return last ? `${data.length}|${data[0].time.getTime()}|${last.time.getTime()}` : "empty";
3774
+ };
3775
+ const getAlignedCompareValues = (state) => {
3776
+ const fingerprint = `${mainDataFingerprint()}|${state.times.length}|${state.times[state.times.length - 1] ?? 0}`;
3777
+ if (state.aligned && state.alignedFingerprint === fingerprint) {
3778
+ return state.aligned;
3779
+ }
3780
+ const aligned = new Float64Array(data.length).fill(Number.NaN);
3781
+ let cursor = 0;
3782
+ let lastValue = Number.NaN;
3783
+ for (let i = 0; i < data.length; i += 1) {
3784
+ const barTime = data[i].time.getTime();
3785
+ while (cursor < state.times.length && state.times[cursor] <= barTime) {
3786
+ lastValue = state.closes[cursor];
3787
+ cursor += 1;
3788
+ }
3789
+ aligned[i] = lastValue;
3790
+ }
3791
+ state.aligned = aligned;
3792
+ state.alignedFingerprint = fingerprint;
3793
+ return aligned;
3794
+ };
3795
+ const visibleCompareSeries = () => compareSeriesStates.filter((state) => state.options.visible !== false && state.times.length > 0);
3796
+ const setCompareSeries = (series) => {
3797
+ compareSeriesStates = (series ?? []).filter((entry) => entry && entry.id).map(prepareCompareSeries);
3798
+ scheduleDraw({ updateAutoScale: true });
3799
+ };
3800
+ const addCompareSeries = (series) => {
3801
+ if (!series?.id) return;
3802
+ compareSeriesStates = [...compareSeriesStates.filter((state) => state.options.id !== series.id), prepareCompareSeries(series)];
3803
+ scheduleDraw({ updateAutoScale: true });
3804
+ };
3805
+ const removeCompareSeries = (id) => {
3806
+ const next = compareSeriesStates.filter((state) => state.options.id !== id);
3807
+ if (next.length === compareSeriesStates.length) return;
3808
+ compareSeriesStates = next;
3809
+ scheduleDraw({ updateAutoScale: true });
3810
+ };
3811
+ const getCompareSeries = () => compareSeriesStates.map((state) => state.options);
3702
3812
  const formatHoverTimeLabel = (time, mode) => {
3703
3813
  if (mode === "time") {
3704
3814
  return time.toLocaleTimeString(void 0, {
@@ -4304,6 +4414,31 @@ function createChart(element, options = {}) {
4304
4414
  const chartType = mergedOptions.chartType;
4305
4415
  const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
4306
4416
  const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
4417
+ const comparePlans = [];
4418
+ for (const state of visibleCompareSeries()) {
4419
+ const aligned = getAlignedCompareValues(state);
4420
+ let anchorIndex = -1;
4421
+ for (let index = startIndex; index <= endIndex; index += 1) {
4422
+ if (Number.isFinite(aligned[index] ?? Number.NaN)) {
4423
+ anchorIndex = index;
4424
+ break;
4425
+ }
4426
+ }
4427
+ if (anchorIndex < 0) continue;
4428
+ const baseCompare = aligned[anchorIndex];
4429
+ const baseMain = seriesData[anchorIndex]?.c ?? 0;
4430
+ const usePercent = (state.options.scale ?? "percent") !== "price";
4431
+ const toPrice = usePercent && baseCompare > 0 ? (value) => baseMain * value / baseCompare : (value) => value;
4432
+ let lastNativeValue = Number.NaN;
4433
+ for (let index = endIndex; index >= startIndex; index -= 1) {
4434
+ const value = aligned[index];
4435
+ if (value !== void 0 && Number.isFinite(value)) {
4436
+ lastNativeValue = value;
4437
+ break;
4438
+ }
4439
+ }
4440
+ comparePlans.push({ state, aligned, toPrice, usePercent, baseCompare, lastNativeValue });
4441
+ }
4307
4442
  const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
4308
4443
  const scanCandleRange = (from, to) => {
4309
4444
  let min = Number.POSITIVE_INFINITY;
@@ -4349,6 +4484,23 @@ function createChart(element, options = {}) {
4349
4484
  maxPrice = Math.max(maxPrice, weightedMax);
4350
4485
  }
4351
4486
  }
4487
+ for (const plan of comparePlans) {
4488
+ if (plan.state.options.includeInAutoScale === false) continue;
4489
+ let compareMin = Number.POSITIVE_INFINITY;
4490
+ let compareMax = Number.NEGATIVE_INFINITY;
4491
+ for (let index = startIndex; index <= endIndex; index += 1) {
4492
+ const value = plan.aligned[index];
4493
+ if (value === void 0 || !Number.isFinite(value)) continue;
4494
+ const price = plan.toPrice(value);
4495
+ if (!Number.isFinite(price)) continue;
4496
+ if (price < compareMin) compareMin = price;
4497
+ if (price > compareMax) compareMax = price;
4498
+ }
4499
+ if (compareMin <= compareMax) {
4500
+ minPrice = Math.min(minPrice, compareMin);
4501
+ maxPrice = Math.max(maxPrice, compareMax);
4502
+ }
4503
+ }
4352
4504
  const priceRange = maxPrice - minPrice || 1;
4353
4505
  const autoMin = minPrice - priceRange * 0.08;
4354
4506
  const autoMax = maxPrice + priceRange * 0.08;
@@ -5460,7 +5612,106 @@ function createChart(element, options = {}) {
5460
5612
  }
5461
5613
  return data[index]?.v;
5462
5614
  };
5463
- if (isLineStyleChart) {
5615
+ const downsamplingOptions = mergedOptions.downsampling ?? DEFAULT_OPTIONS.downsampling;
5616
+ const downsampleThresholdPx = Math.max(0.2, downsamplingOptions?.thresholdPx ?? 1.5);
5617
+ const shouldDownsample = (downsamplingOptions?.enabled ?? true) && candleSpacing < downsampleThresholdPx && endIndex - startIndex > 2;
5618
+ const seriesColumns = [];
5619
+ if (shouldDownsample) {
5620
+ let current = null;
5621
+ let currentColumn = Number.NaN;
5622
+ for (let index = startIndex; index <= endIndex; index += 1) {
5623
+ const point = seriesData[index];
5624
+ if (!point) continue;
5625
+ const column = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
5626
+ if (!current || column !== currentColumn) {
5627
+ currentColumn = column;
5628
+ current = {
5629
+ x: column,
5630
+ open: point.o,
5631
+ high: point.h,
5632
+ low: point.l,
5633
+ close: point.c,
5634
+ minClose: point.c,
5635
+ maxClose: point.c,
5636
+ minCloseIndex: index,
5637
+ maxCloseIndex: index
5638
+ };
5639
+ seriesColumns.push(current);
5640
+ continue;
5641
+ }
5642
+ if (point.h > current.high) current.high = point.h;
5643
+ if (point.l < current.low) current.low = point.l;
5644
+ current.close = point.c;
5645
+ if (point.c < current.minClose) {
5646
+ current.minClose = point.c;
5647
+ current.minCloseIndex = index;
5648
+ }
5649
+ if (point.c > current.maxClose) {
5650
+ current.maxClose = point.c;
5651
+ current.maxCloseIndex = index;
5652
+ }
5653
+ }
5654
+ }
5655
+ if (shouldDownsample && !isLineStyleChart) {
5656
+ const upColumns = new Path2D();
5657
+ const downColumns = new Path2D();
5658
+ for (const column of seriesColumns) {
5659
+ const path = column.close >= column.open ? upColumns : downColumns;
5660
+ const x = column.x + 0.5;
5661
+ const top = crisp(yFromPrice(column.high));
5662
+ const bottom = crisp(yFromPrice(column.low));
5663
+ path.moveTo(x, top);
5664
+ path.lineTo(x, bottom === top ? bottom + 1 : bottom);
5665
+ }
5666
+ ctx.lineWidth = 1;
5667
+ ctx.strokeStyle = mergedOptions.upColor;
5668
+ ctx.stroke(upColumns);
5669
+ ctx.strokeStyle = mergedOptions.downColor;
5670
+ ctx.stroke(downColumns);
5671
+ } else if (shouldDownsample && isLineStyleChart) {
5672
+ const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5673
+ const linePath = new Path2D();
5674
+ let started = false;
5675
+ let firstX = 0;
5676
+ let lastX = 0;
5677
+ for (const column of seriesColumns) {
5678
+ const lowFirst = column.minCloseIndex <= column.maxCloseIndex;
5679
+ const first = lowFirst ? column.minClose : column.maxClose;
5680
+ const second = lowFirst ? column.maxClose : column.minClose;
5681
+ const x = column.x;
5682
+ if (!started) {
5683
+ linePath.moveTo(x, yFromPrice(first));
5684
+ firstX = x;
5685
+ started = true;
5686
+ } else {
5687
+ linePath.lineTo(x, yFromPrice(first));
5688
+ }
5689
+ if (second !== first) {
5690
+ linePath.lineTo(x, yFromPrice(second));
5691
+ }
5692
+ lastX = x;
5693
+ }
5694
+ if (started) {
5695
+ if (chartType === "area" || chartType === "baseline") {
5696
+ const baseY = chartType === "baseline" ? yFromPrice(mergedOptions.baselinePrice ?? (yMin + yMax) / 2) : chartBottom;
5697
+ const fillPath = new Path2D(linePath);
5698
+ fillPath.lineTo(lastX, baseY);
5699
+ fillPath.lineTo(firstX, baseY);
5700
+ fillPath.closePath();
5701
+ ctx.save();
5702
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
5703
+ ctx.fillStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5704
+ ctx.fill(fillPath);
5705
+ ctx.restore();
5706
+ }
5707
+ ctx.save();
5708
+ ctx.strokeStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5709
+ ctx.lineWidth = seriesLineWidth;
5710
+ ctx.lineJoin = "round";
5711
+ ctx.stroke(linePath);
5712
+ ctx.restore();
5713
+ }
5714
+ } else if (isLineStyleChart) {
5464
5715
  const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5465
5716
  const linePath = new Path2D();
5466
5717
  let started = false;
@@ -5626,6 +5877,105 @@ function createChart(element, options = {}) {
5626
5877
  ctx.stroke(downHollowBodies);
5627
5878
  }
5628
5879
  }
5880
+ const compareAxisTags = [];
5881
+ for (const plan of comparePlans) {
5882
+ const color = plan.state.options.color ?? "#f59e0b";
5883
+ const lineWidth = Math.max(1, plan.state.options.lineWidth ?? 1.5);
5884
+ const path = new Path2D();
5885
+ let started = false;
5886
+ let firstX = 0;
5887
+ let lastX = 0;
5888
+ let lastY = 0;
5889
+ let lastPrice = Number.NaN;
5890
+ const emit = (x, y) => {
5891
+ if (!started) {
5892
+ path.moveTo(x, y);
5893
+ firstX = x;
5894
+ started = true;
5895
+ } else {
5896
+ path.lineTo(x, y);
5897
+ }
5898
+ lastX = x;
5899
+ lastY = y;
5900
+ };
5901
+ const xForIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
5902
+ if (shouldDownsample) {
5903
+ let column = Number.NaN;
5904
+ let minValue = 0;
5905
+ let maxValue = 0;
5906
+ let minIndex = 0;
5907
+ let maxIndex = 0;
5908
+ let hasColumn = false;
5909
+ const flushColumn = () => {
5910
+ if (!hasColumn) return;
5911
+ const lowFirst = minIndex <= maxIndex;
5912
+ const first = plan.toPrice(lowFirst ? minValue : maxValue);
5913
+ const second = plan.toPrice(lowFirst ? maxValue : minValue);
5914
+ if (Number.isFinite(first)) emit(column, yFromPrice(first));
5915
+ if (Number.isFinite(second) && second !== first) emit(column, yFromPrice(second));
5916
+ lastPrice = plan.toPrice(maxIndex >= minIndex ? maxValue : minValue);
5917
+ };
5918
+ for (let index = startIndex; index <= endIndex; index += 1) {
5919
+ const value = plan.aligned[index];
5920
+ if (value === void 0 || !Number.isFinite(value)) continue;
5921
+ const nextColumn = Math.round(xForIndex(index));
5922
+ if (!hasColumn || nextColumn !== column) {
5923
+ flushColumn();
5924
+ column = nextColumn;
5925
+ minValue = value;
5926
+ maxValue = value;
5927
+ minIndex = index;
5928
+ maxIndex = index;
5929
+ hasColumn = true;
5930
+ continue;
5931
+ }
5932
+ if (value < minValue) {
5933
+ minValue = value;
5934
+ minIndex = index;
5935
+ }
5936
+ if (value > maxValue) {
5937
+ maxValue = value;
5938
+ maxIndex = index;
5939
+ }
5940
+ }
5941
+ flushColumn();
5942
+ } else {
5943
+ for (let index = startIndex; index <= endIndex; index += 1) {
5944
+ const value = plan.aligned[index];
5945
+ if (value === void 0 || !Number.isFinite(value)) continue;
5946
+ const price = plan.toPrice(value);
5947
+ if (!Number.isFinite(price)) continue;
5948
+ lastPrice = price;
5949
+ emit(xForIndex(index), yFromPrice(price));
5950
+ }
5951
+ }
5952
+ if (!started) continue;
5953
+ if (plan.state.options.style === "area") {
5954
+ const fillPath = new Path2D(path);
5955
+ fillPath.lineTo(lastX, chartBottom);
5956
+ fillPath.lineTo(firstX, chartBottom);
5957
+ fillPath.closePath();
5958
+ ctx.save();
5959
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity)) * 0.6;
5960
+ ctx.fillStyle = color;
5961
+ ctx.fill(fillPath);
5962
+ ctx.restore();
5963
+ }
5964
+ ctx.save();
5965
+ ctx.strokeStyle = color;
5966
+ ctx.lineWidth = lineWidth;
5967
+ ctx.lineJoin = "round";
5968
+ ctx.stroke(path);
5969
+ ctx.restore();
5970
+ if (Number.isFinite(lastPrice)) {
5971
+ const label = plan.state.options.label ?? plan.state.options.id;
5972
+ const nativeLast = plan.lastNativeValue;
5973
+ const usePercentTag = plan.usePercent && plan.baseCompare > 0 && Number.isFinite(nativeLast);
5974
+ const changePct = usePercentTag ? (nativeLast / plan.baseCompare - 1) * 100 : 0;
5975
+ const shortText = usePercentTag ? `${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%` : formatPrice(nativeLast);
5976
+ compareAxisTags.push({ y: lastY, text: `${label} ${shortText}`, shortText, color });
5977
+ }
5978
+ }
5629
5979
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
5630
5980
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
5631
5981
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
@@ -6400,6 +6750,35 @@ function createChart(element, options = {}) {
6400
6750
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
6401
6751
  ctx.font = prevFont;
6402
6752
  }
6753
+ if (compareAxisTags.length > 0 && width - chartRight > 0) {
6754
+ const prevFont = ctx.font;
6755
+ const tagFontSize = Math.max(9, (resolvedAxis.fontSize ?? 12) - 2);
6756
+ ctx.font = `${tagFontSize}px ${mergedOptions.fontFamily}`;
6757
+ ctx.textBaseline = "middle";
6758
+ ctx.textAlign = "left";
6759
+ const placed = [];
6760
+ const tagHeight = tagFontSize + 6;
6761
+ const gutterWidth = width - chartRight - 4;
6762
+ for (const tag of [...compareAxisTags].sort((a, b) => a.y - b.y)) {
6763
+ const text = measureTextWidth(tag.text) + 10 <= gutterWidth ? tag.text : tag.shortText;
6764
+ const tagWidth = Math.min(gutterWidth, measureTextWidth(text) + 10);
6765
+ if (tagWidth <= 0) continue;
6766
+ let tagY = clamp(tag.y - tagHeight / 2, chartTop, chartBottom - tagHeight);
6767
+ for (const slot of placed) {
6768
+ if (tagY < slot.bottom + 2 && tagY + tagHeight > slot.top - 2) {
6769
+ tagY = slot.bottom + 2;
6770
+ }
6771
+ }
6772
+ placed.push({ top: tagY, bottom: tagY + tagHeight });
6773
+ ctx.save();
6774
+ ctx.fillStyle = tag.color;
6775
+ fillRoundedRect(chartRight + 2, Math.round(tagY), Math.round(tagWidth), tagHeight, 3);
6776
+ ctx.fillStyle = labelTextColorOn(tag.color, "#ffffff");
6777
+ ctx.fillText(text, chartRight + 7, Math.round(tagY + tagHeight / 2));
6778
+ ctx.restore();
6779
+ }
6780
+ ctx.font = prevFont;
6781
+ }
6403
6782
  if (baseCtx) {
6404
6783
  if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
6405
6784
  baseCanvas.width = canvas.width;
@@ -6423,6 +6802,7 @@ function createChart(element, options = {}) {
6423
6802
  xSpan
6424
6803
  };
6425
6804
  paintCrosshairOverlay();
6805
+ emitSelectionChangeIfMoved();
6426
6806
  };
6427
6807
  const paintCrosshairOverlay = () => {
6428
6808
  crosshairPriceActionRegion = null;
@@ -7023,6 +7403,98 @@ function createChart(element, options = {}) {
7023
7403
  const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7024
7404
  return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
7025
7405
  };
7406
+ const getDrawingBounds = (drawing) => {
7407
+ if (!drawState || drawing.points.length === 0) {
7408
+ return null;
7409
+ }
7410
+ const { chartLeft, chartRight, chartTop, chartBottom } = drawState;
7411
+ let left;
7412
+ let right;
7413
+ let top;
7414
+ let bottom;
7415
+ if (drawing.type === "horizontal-line") {
7416
+ const y = canvasYFromDrawingPrice(drawing.points[0].price);
7417
+ left = chartLeft;
7418
+ right = chartRight;
7419
+ top = y;
7420
+ bottom = y;
7421
+ } else if (drawing.type === "vertical-line") {
7422
+ const x = canvasXFromDrawingPoint(drawing.points[0]);
7423
+ left = x;
7424
+ right = x;
7425
+ top = chartTop;
7426
+ bottom = chartBottom;
7427
+ } else {
7428
+ const xs = drawing.points.map((point) => canvasXFromDrawingPoint(point));
7429
+ const ys = drawing.points.map((point) => canvasYFromDrawingPrice(point.price));
7430
+ left = Math.min(...xs);
7431
+ right = Math.max(...xs);
7432
+ top = Math.min(...ys);
7433
+ bottom = Math.max(...ys);
7434
+ if (drawing.type === "ray") {
7435
+ right = chartRight;
7436
+ }
7437
+ }
7438
+ const clampX = (value) => clamp(value, chartLeft, chartRight);
7439
+ const clampY = (value) => clamp(value, chartTop, chartBottom);
7440
+ left = clampX(left);
7441
+ right = clampX(right);
7442
+ top = clampY(top);
7443
+ bottom = clampY(bottom);
7444
+ return {
7445
+ left,
7446
+ top,
7447
+ right,
7448
+ bottom,
7449
+ centerX: (left + right) / 2,
7450
+ centerY: (top + bottom) / 2
7451
+ };
7452
+ };
7453
+ const getSelectedDrawingWithBounds = () => {
7454
+ if (!selectedDrawingId) {
7455
+ return null;
7456
+ }
7457
+ const drawing = drawings.find((entry) => entry.id === selectedDrawingId);
7458
+ if (!drawing || !drawing.visible) {
7459
+ return null;
7460
+ }
7461
+ const bounds = getDrawingBounds(drawing);
7462
+ return bounds ? { drawing: serializeDrawing(drawing), bounds } : null;
7463
+ };
7464
+ const emitSelectionChangeIfMoved = () => {
7465
+ if (!selectionChangeHandler) {
7466
+ lastSelectionSignature = null;
7467
+ return;
7468
+ }
7469
+ const selection = getSelectedDrawingWithBounds();
7470
+ if (!selection || !drawState) {
7471
+ if (lastSelectionSignature !== null) {
7472
+ lastSelectionSignature = null;
7473
+ selectionChangeHandler(null);
7474
+ }
7475
+ return;
7476
+ }
7477
+ const dragging = drawingDragState !== null;
7478
+ const { bounds } = selection;
7479
+ const signature = `${selection.drawing.id}:${Math.round(bounds.left)}:${Math.round(bounds.top)}:${Math.round(
7480
+ bounds.right
7481
+ )}:${Math.round(bounds.bottom)}:${dragging ? 1 : 0}`;
7482
+ if (signature === lastSelectionSignature) {
7483
+ return;
7484
+ }
7485
+ lastSelectionSignature = signature;
7486
+ selectionChangeHandler({
7487
+ drawing: selection.drawing,
7488
+ bounds,
7489
+ plot: {
7490
+ left: drawState.chartLeft,
7491
+ top: drawState.chartTop,
7492
+ right: drawState.chartRight,
7493
+ bottom: drawState.chartBottom
7494
+ },
7495
+ dragging
7496
+ });
7497
+ };
7026
7498
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
7027
7499
  const dx = x2 - x1;
7028
7500
  const dy = y2 - y1;
@@ -7989,6 +8461,10 @@ function createChart(element, options = {}) {
7989
8461
  if (event.pointerType === "touch" || event.pointerType === "pen") {
7990
8462
  event.preventDefault();
7991
8463
  }
8464
+ if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
8465
+ focusFromPointer = true;
8466
+ canvas.focus({ preventScroll: true });
8467
+ }
7992
8468
  cancelKineticPan();
7993
8469
  panVelocitySamples.length = 0;
7994
8470
  magnetModifierActive = event.metaKey || event.ctrlKey;
@@ -8106,12 +8582,14 @@ function createChart(element, options = {}) {
8106
8582
  const drawingHit = getDrawingHit(point.x, point.y);
8107
8583
  if (drawingHit) {
8108
8584
  selectedDrawingId = drawingHit.drawing.id;
8585
+ const selectBounds = getDrawingBounds(drawingHit.drawing);
8109
8586
  drawingSelectHandler?.({
8110
8587
  drawing: serializeDrawing(drawingHit.drawing),
8111
8588
  target: drawingHit.target,
8112
8589
  ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
8113
8590
  x: point.x,
8114
- y: point.y
8591
+ y: point.y,
8592
+ ...selectBounds ? { bounds: selectBounds } : {}
8115
8593
  });
8116
8594
  if (!drawingHit.drawing.locked) {
8117
8595
  const startCanvasPoint = drawingPointFromCanvas(point.x, point.y, false);
@@ -8672,7 +9150,152 @@ function createChart(element, options = {}) {
8672
9150
  };
8673
9151
  const onContextMenu = (event) => {
8674
9152
  event.preventDefault();
9153
+ if (!contextMenuHandler || !drawState) {
9154
+ return;
9155
+ }
9156
+ const point = getCanvasPoint(event);
9157
+ const hitRegion = getHitRegion(point.x, point.y);
9158
+ if (hitRegion === "outside") {
9159
+ return;
9160
+ }
9161
+ const pane = hitRegion === "plot" ? getPaneAt(point.x, point.y) : null;
9162
+ const drawingHit = hitRegion === "plot" && !pane ? getDrawingHit(point.x, point.y) : null;
9163
+ if (drawingHit) {
9164
+ if (selectedDrawingId !== drawingHit.drawing.id) {
9165
+ selectedDrawingId = drawingHit.drawing.id;
9166
+ scheduleDraw();
9167
+ }
9168
+ }
9169
+ const region = drawingHit ? "drawing" : pane ? "indicator-pane" : hitRegion;
9170
+ const index = indexFromCanvasX(point.x);
9171
+ const barTime = index === null ? null : getTimeForIndex(index);
9172
+ const bar = index === null ? void 0 : data[index];
9173
+ const inMainPane = !pane && (hitRegion === "plot" || hitRegion === "y-axis");
9174
+ contextMenuHandler({
9175
+ region,
9176
+ x: point.x,
9177
+ y: point.y,
9178
+ clientX: event.clientX,
9179
+ clientY: event.clientY,
9180
+ ...inMainPane ? { price: roundToPricePrecision(priceFromCanvasY(point.y)) } : {},
9181
+ ...index === null ? {} : { index },
9182
+ ...barTime ? { time: barTime.toISOString() } : {},
9183
+ ...bar ? {
9184
+ point: {
9185
+ t: bar.time.toISOString(),
9186
+ o: bar.o,
9187
+ h: bar.h,
9188
+ l: bar.l,
9189
+ c: bar.c,
9190
+ ...bar.v === void 0 ? {} : { v: bar.v }
9191
+ }
9192
+ } : {},
9193
+ ...drawingHit ? {
9194
+ drawing: serializeDrawing(drawingHit.drawing),
9195
+ drawingTarget: drawingHit.target,
9196
+ ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex }
9197
+ } : {},
9198
+ ...pane ? { indicator: { id: pane.id, type: pane.type } } : {}
9199
+ });
9200
+ };
9201
+ const onCanvasKeyDown = (event) => {
9202
+ const keyboard = mergedOptions.keyboard ?? {};
9203
+ if (keyboard.enabled === false) {
9204
+ return;
9205
+ }
9206
+ if (event.altKey || event.ctrlKey || event.metaKey) {
9207
+ return;
9208
+ }
9209
+ const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
9210
+ const emit = (action, drawing) => {
9211
+ keyboardShortcutHandler?.({
9212
+ action,
9213
+ key: event.key,
9214
+ shiftKey: event.shiftKey,
9215
+ ...drawing ? { drawing } : {}
9216
+ });
9217
+ };
9218
+ switch (event.key) {
9219
+ case "Delete":
9220
+ case "Backspace": {
9221
+ if (keyboard.deleteSelectedDrawing === false || !selectedDrawingId) {
9222
+ return;
9223
+ }
9224
+ const target = drawings.find((entry) => entry.id === selectedDrawingId);
9225
+ if (!target) {
9226
+ return;
9227
+ }
9228
+ const removed = serializeDrawing(target);
9229
+ removeDrawing(selectedDrawingId);
9230
+ selectedDrawingId = null;
9231
+ scheduleDraw();
9232
+ emit("delete-drawing", removed);
9233
+ break;
9234
+ }
9235
+ case "Escape": {
9236
+ if (cancelDrawing()) {
9237
+ emit("cancel");
9238
+ break;
9239
+ }
9240
+ if (activeDrawingTool) {
9241
+ setActiveDrawingTool(null);
9242
+ emit("cancel");
9243
+ break;
9244
+ }
9245
+ if (selectedDrawingId) {
9246
+ setSelectedDrawing(null);
9247
+ emit("cancel");
9248
+ break;
9249
+ }
9250
+ return;
9251
+ }
9252
+ case "ArrowLeft":
9253
+ panX(-panStep);
9254
+ emit("pan-left");
9255
+ break;
9256
+ case "ArrowRight":
9257
+ panX(panStep);
9258
+ emit("pan-right");
9259
+ break;
9260
+ case "ArrowUp":
9261
+ case "ArrowDown": {
9262
+ if (!drawState) {
9263
+ return;
9264
+ }
9265
+ const step = (drawState.yMax - drawState.yMin) / 20 * (event.shiftKey ? 5 : 1);
9266
+ panY(event.key === "ArrowUp" ? step : -step);
9267
+ emit(event.key === "ArrowUp" ? "pan-up" : "pan-down");
9268
+ break;
9269
+ }
9270
+ case "+":
9271
+ case "=":
9272
+ zoomInX();
9273
+ emit("zoom-in");
9274
+ break;
9275
+ case "-":
9276
+ case "_":
9277
+ zoomOutX();
9278
+ emit("zoom-out");
9279
+ break;
9280
+ case "Home":
9281
+ fitContent();
9282
+ emit("reset-viewport");
9283
+ break;
9284
+ case "End":
9285
+ setFollowingLatest(true);
9286
+ emit("scroll-to-realtime");
9287
+ break;
9288
+ case "r":
9289
+ case "R":
9290
+ resetViewport();
9291
+ emit("reset-viewport");
9292
+ break;
9293
+ default:
9294
+ return;
9295
+ }
9296
+ event.preventDefault();
8675
9297
  };
9298
+ canvas.addEventListener("keydown", onCanvasKeyDown);
8676
9299
  canvas.addEventListener("pointerdown", onPointerDown);
8677
9300
  canvas.addEventListener("pointermove", onPointerMove);
8678
9301
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -8719,6 +9342,7 @@ function createChart(element, options = {}) {
8719
9342
  resetRightMarginCache();
8720
9343
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
8721
9344
  doubleClickAction = mergedOptions.doubleClickAction;
9345
+ applyAccessibilityAttributes();
8722
9346
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
8723
9347
  if (!isTickerSmoothingEnabled) {
8724
9348
  smoothedTickerPrice = null;
@@ -9102,6 +9726,23 @@ function createChart(element, options = {}) {
9102
9726
  const onDrawingHover = (handler) => {
9103
9727
  drawingHoverHandler = handler;
9104
9728
  };
9729
+ const onSelectionChange = (handler) => {
9730
+ selectionChangeHandler = handler;
9731
+ lastSelectionSignature = null;
9732
+ if (handler) {
9733
+ emitSelectionChangeIfMoved();
9734
+ }
9735
+ };
9736
+ const getSelectedDrawing = () => getSelectedDrawingWithBounds();
9737
+ const onContextMenuHandler = (handler) => {
9738
+ contextMenuHandler = handler;
9739
+ };
9740
+ const onKeyboardShortcut = (handler) => {
9741
+ keyboardShortcutHandler = handler;
9742
+ };
9743
+ const focus = () => {
9744
+ canvas.focus({ preventScroll: true });
9745
+ };
9105
9746
  const saveState = () => {
9106
9747
  const drawingDefaults = {};
9107
9748
  for (const [tool, defaults] of drawingToolDefaults) {
@@ -9166,6 +9807,7 @@ function createChart(element, options = {}) {
9166
9807
  drawRafId = null;
9167
9808
  pendingDrawUpdateAutoScale = false;
9168
9809
  }
9810
+ canvas.removeEventListener("keydown", onCanvasKeyDown);
9169
9811
  canvas.removeEventListener("pointerdown", onPointerDown);
9170
9812
  canvas.removeEventListener("pointermove", onPointerMove);
9171
9813
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -9229,10 +9871,19 @@ function createChart(element, options = {}) {
9229
9871
  onDrawingSelect,
9230
9872
  onDrawingDoubleClick,
9231
9873
  onDrawingEditText,
9874
+ onSelectionChange,
9875
+ getSelectedDrawing,
9876
+ onContextMenu: onContextMenuHandler,
9877
+ onKeyboardShortcut,
9878
+ focus,
9232
9879
  setMagnetMode,
9233
9880
  getMagnetMode,
9234
9881
  setPriceScale,
9235
9882
  getPriceScale,
9883
+ setCompareSeries,
9884
+ addCompareSeries,
9885
+ removeCompareSeries,
9886
+ getCompareSeries,
9236
9887
  cancelDrawing,
9237
9888
  setTradeMarkers,
9238
9889
  setSelectedDrawing,