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.
@@ -2851,6 +2851,9 @@ var DEFAULT_OPTIONS = {
2851
2851
  pinOutOfRangeLines: false,
2852
2852
  doubleClickEnabled: true,
2853
2853
  doubleClickAction: "reset",
2854
+ keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2855
+ accessibility: { label: "Price chart", description: "", focusable: true },
2856
+ downsampling: { enabled: true, thresholdPx: 1.5 },
2854
2857
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2855
2858
  grid: DEFAULT_GRID_OPTIONS,
2856
2859
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2893,6 +2896,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2893
2896
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
2894
2897
  ...options.yAxis ?? {}
2895
2898
  },
2899
+ keyboard: {
2900
+ ...baseOptions.keyboard,
2901
+ ...options.keyboard ?? {}
2902
+ },
2903
+ accessibility: {
2904
+ ...baseOptions.accessibility,
2905
+ ...options.accessibility ?? {}
2906
+ },
2907
+ downsampling: {
2908
+ ...baseOptions.downsampling,
2909
+ ...options.downsampling ?? {}
2910
+ },
2896
2911
  crosshair: {
2897
2912
  ...baseOptions.crosshair,
2898
2913
  ...options.crosshair ?? {}
@@ -3066,6 +3081,10 @@ function createChart(element, options = {}) {
3066
3081
  let drawingSelectHandler = null;
3067
3082
  let drawingDoubleClickHandler = null;
3068
3083
  let drawingEditTextHandler = null;
3084
+ let selectionChangeHandler = null;
3085
+ let contextMenuHandler = null;
3086
+ let keyboardShortcutHandler = null;
3087
+ let lastSelectionSignature = null;
3069
3088
  let magnetMode = "none";
3070
3089
  let priceScaleMode = "linear";
3071
3090
  let priceScaleInverted = false;
@@ -3228,6 +3247,39 @@ function createChart(element, options = {}) {
3228
3247
  canvas.style.setProperty("-webkit-user-select", "none");
3229
3248
  canvas.style.setProperty("-webkit-touch-callout", "none");
3230
3249
  canvas.setAttribute("draggable", "false");
3250
+ const applyAccessibilityAttributes = () => {
3251
+ const a11y = mergedOptions.accessibility ?? {};
3252
+ const focusable = a11y.focusable !== false;
3253
+ if (focusable) {
3254
+ canvas.tabIndex = 0;
3255
+ } else {
3256
+ canvas.removeAttribute("tabindex");
3257
+ }
3258
+ canvas.setAttribute("role", "application");
3259
+ canvas.setAttribute("aria-label", a11y.label ?? "Price chart");
3260
+ if (a11y.description) {
3261
+ canvas.setAttribute("aria-description", a11y.description);
3262
+ } else {
3263
+ canvas.removeAttribute("aria-description");
3264
+ }
3265
+ if (focusable && (mergedOptions.keyboard?.enabled ?? true)) {
3266
+ canvas.setAttribute(
3267
+ "aria-keyshortcuts",
3268
+ "ArrowLeft ArrowRight ArrowUp ArrowDown Plus Minus Home End Delete Escape"
3269
+ );
3270
+ } else {
3271
+ canvas.removeAttribute("aria-keyshortcuts");
3272
+ }
3273
+ };
3274
+ applyAccessibilityAttributes();
3275
+ let focusFromPointer = false;
3276
+ canvas.addEventListener("focus", () => {
3277
+ canvas.style.outline = focusFromPointer ? "none" : "";
3278
+ focusFromPointer = false;
3279
+ });
3280
+ canvas.addEventListener("blur", () => {
3281
+ canvas.style.outline = "none";
3282
+ });
3231
3283
  element.innerHTML = "";
3232
3284
  element.appendChild(canvas);
3233
3285
  const baseCanvas = document.createElement("canvas");
@@ -3731,6 +3783,64 @@ function createChart(element, options = {}) {
3731
3783
  heikinAshiFingerprint = fingerprint;
3732
3784
  return result;
3733
3785
  };
3786
+ let compareSeriesStates = [];
3787
+ const prepareCompareSeries = (options2) => {
3788
+ const rows = [];
3789
+ for (const point of options2.data ?? []) {
3790
+ const t = new Date(point.t).getTime();
3791
+ const c = Number(point.c);
3792
+ if (Number.isFinite(t) && Number.isFinite(c)) rows.push({ t, c });
3793
+ }
3794
+ rows.sort((a, b) => a.t - b.t);
3795
+ return {
3796
+ options: options2,
3797
+ times: rows.map((row) => row.t),
3798
+ closes: rows.map((row) => row.c),
3799
+ aligned: null,
3800
+ alignedFingerprint: ""
3801
+ };
3802
+ };
3803
+ const mainDataFingerprint = () => {
3804
+ const last = data[data.length - 1];
3805
+ return last ? `${data.length}|${data[0].time.getTime()}|${last.time.getTime()}` : "empty";
3806
+ };
3807
+ const getAlignedCompareValues = (state) => {
3808
+ const fingerprint = `${mainDataFingerprint()}|${state.times.length}|${state.times[state.times.length - 1] ?? 0}`;
3809
+ if (state.aligned && state.alignedFingerprint === fingerprint) {
3810
+ return state.aligned;
3811
+ }
3812
+ const aligned = new Float64Array(data.length).fill(Number.NaN);
3813
+ let cursor = 0;
3814
+ let lastValue = Number.NaN;
3815
+ for (let i = 0; i < data.length; i += 1) {
3816
+ const barTime = data[i].time.getTime();
3817
+ while (cursor < state.times.length && state.times[cursor] <= barTime) {
3818
+ lastValue = state.closes[cursor];
3819
+ cursor += 1;
3820
+ }
3821
+ aligned[i] = lastValue;
3822
+ }
3823
+ state.aligned = aligned;
3824
+ state.alignedFingerprint = fingerprint;
3825
+ return aligned;
3826
+ };
3827
+ const visibleCompareSeries = () => compareSeriesStates.filter((state) => state.options.visible !== false && state.times.length > 0);
3828
+ const setCompareSeries = (series) => {
3829
+ compareSeriesStates = (series ?? []).filter((entry) => entry && entry.id).map(prepareCompareSeries);
3830
+ scheduleDraw({ updateAutoScale: true });
3831
+ };
3832
+ const addCompareSeries = (series) => {
3833
+ if (!series?.id) return;
3834
+ compareSeriesStates = [...compareSeriesStates.filter((state) => state.options.id !== series.id), prepareCompareSeries(series)];
3835
+ scheduleDraw({ updateAutoScale: true });
3836
+ };
3837
+ const removeCompareSeries = (id) => {
3838
+ const next = compareSeriesStates.filter((state) => state.options.id !== id);
3839
+ if (next.length === compareSeriesStates.length) return;
3840
+ compareSeriesStates = next;
3841
+ scheduleDraw({ updateAutoScale: true });
3842
+ };
3843
+ const getCompareSeries = () => compareSeriesStates.map((state) => state.options);
3734
3844
  const formatHoverTimeLabel = (time, mode) => {
3735
3845
  if (mode === "time") {
3736
3846
  return time.toLocaleTimeString(void 0, {
@@ -4336,6 +4446,31 @@ function createChart(element, options = {}) {
4336
4446
  const chartType = mergedOptions.chartType;
4337
4447
  const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
4338
4448
  const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
4449
+ const comparePlans = [];
4450
+ for (const state of visibleCompareSeries()) {
4451
+ const aligned = getAlignedCompareValues(state);
4452
+ let anchorIndex = -1;
4453
+ for (let index = startIndex; index <= endIndex; index += 1) {
4454
+ if (Number.isFinite(aligned[index] ?? Number.NaN)) {
4455
+ anchorIndex = index;
4456
+ break;
4457
+ }
4458
+ }
4459
+ if (anchorIndex < 0) continue;
4460
+ const baseCompare = aligned[anchorIndex];
4461
+ const baseMain = seriesData[anchorIndex]?.c ?? 0;
4462
+ const usePercent = (state.options.scale ?? "percent") !== "price";
4463
+ const toPrice = usePercent && baseCompare > 0 ? (value) => baseMain * value / baseCompare : (value) => value;
4464
+ let lastNativeValue = Number.NaN;
4465
+ for (let index = endIndex; index >= startIndex; index -= 1) {
4466
+ const value = aligned[index];
4467
+ if (value !== void 0 && Number.isFinite(value)) {
4468
+ lastNativeValue = value;
4469
+ break;
4470
+ }
4471
+ }
4472
+ comparePlans.push({ state, aligned, toPrice, usePercent, baseCompare, lastNativeValue });
4473
+ }
4339
4474
  const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
4340
4475
  const scanCandleRange = (from, to) => {
4341
4476
  let min = Number.POSITIVE_INFINITY;
@@ -4381,6 +4516,23 @@ function createChart(element, options = {}) {
4381
4516
  maxPrice = Math.max(maxPrice, weightedMax);
4382
4517
  }
4383
4518
  }
4519
+ for (const plan of comparePlans) {
4520
+ if (plan.state.options.includeInAutoScale === false) continue;
4521
+ let compareMin = Number.POSITIVE_INFINITY;
4522
+ let compareMax = Number.NEGATIVE_INFINITY;
4523
+ for (let index = startIndex; index <= endIndex; index += 1) {
4524
+ const value = plan.aligned[index];
4525
+ if (value === void 0 || !Number.isFinite(value)) continue;
4526
+ const price = plan.toPrice(value);
4527
+ if (!Number.isFinite(price)) continue;
4528
+ if (price < compareMin) compareMin = price;
4529
+ if (price > compareMax) compareMax = price;
4530
+ }
4531
+ if (compareMin <= compareMax) {
4532
+ minPrice = Math.min(minPrice, compareMin);
4533
+ maxPrice = Math.max(maxPrice, compareMax);
4534
+ }
4535
+ }
4384
4536
  const priceRange = maxPrice - minPrice || 1;
4385
4537
  const autoMin = minPrice - priceRange * 0.08;
4386
4538
  const autoMax = maxPrice + priceRange * 0.08;
@@ -5492,7 +5644,106 @@ function createChart(element, options = {}) {
5492
5644
  }
5493
5645
  return data[index]?.v;
5494
5646
  };
5495
- if (isLineStyleChart) {
5647
+ const downsamplingOptions = mergedOptions.downsampling ?? DEFAULT_OPTIONS.downsampling;
5648
+ const downsampleThresholdPx = Math.max(0.2, downsamplingOptions?.thresholdPx ?? 1.5);
5649
+ const shouldDownsample = (downsamplingOptions?.enabled ?? true) && candleSpacing < downsampleThresholdPx && endIndex - startIndex > 2;
5650
+ const seriesColumns = [];
5651
+ if (shouldDownsample) {
5652
+ let current = null;
5653
+ let currentColumn = Number.NaN;
5654
+ for (let index = startIndex; index <= endIndex; index += 1) {
5655
+ const point = seriesData[index];
5656
+ if (!point) continue;
5657
+ const column = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
5658
+ if (!current || column !== currentColumn) {
5659
+ currentColumn = column;
5660
+ current = {
5661
+ x: column,
5662
+ open: point.o,
5663
+ high: point.h,
5664
+ low: point.l,
5665
+ close: point.c,
5666
+ minClose: point.c,
5667
+ maxClose: point.c,
5668
+ minCloseIndex: index,
5669
+ maxCloseIndex: index
5670
+ };
5671
+ seriesColumns.push(current);
5672
+ continue;
5673
+ }
5674
+ if (point.h > current.high) current.high = point.h;
5675
+ if (point.l < current.low) current.low = point.l;
5676
+ current.close = point.c;
5677
+ if (point.c < current.minClose) {
5678
+ current.minClose = point.c;
5679
+ current.minCloseIndex = index;
5680
+ }
5681
+ if (point.c > current.maxClose) {
5682
+ current.maxClose = point.c;
5683
+ current.maxCloseIndex = index;
5684
+ }
5685
+ }
5686
+ }
5687
+ if (shouldDownsample && !isLineStyleChart) {
5688
+ const upColumns = new Path2D();
5689
+ const downColumns = new Path2D();
5690
+ for (const column of seriesColumns) {
5691
+ const path = column.close >= column.open ? upColumns : downColumns;
5692
+ const x = column.x + 0.5;
5693
+ const top = crisp(yFromPrice(column.high));
5694
+ const bottom = crisp(yFromPrice(column.low));
5695
+ path.moveTo(x, top);
5696
+ path.lineTo(x, bottom === top ? bottom + 1 : bottom);
5697
+ }
5698
+ ctx.lineWidth = 1;
5699
+ ctx.strokeStyle = mergedOptions.upColor;
5700
+ ctx.stroke(upColumns);
5701
+ ctx.strokeStyle = mergedOptions.downColor;
5702
+ ctx.stroke(downColumns);
5703
+ } else if (shouldDownsample && isLineStyleChart) {
5704
+ const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5705
+ const linePath = new Path2D();
5706
+ let started = false;
5707
+ let firstX = 0;
5708
+ let lastX = 0;
5709
+ for (const column of seriesColumns) {
5710
+ const lowFirst = column.minCloseIndex <= column.maxCloseIndex;
5711
+ const first = lowFirst ? column.minClose : column.maxClose;
5712
+ const second = lowFirst ? column.maxClose : column.minClose;
5713
+ const x = column.x;
5714
+ if (!started) {
5715
+ linePath.moveTo(x, yFromPrice(first));
5716
+ firstX = x;
5717
+ started = true;
5718
+ } else {
5719
+ linePath.lineTo(x, yFromPrice(first));
5720
+ }
5721
+ if (second !== first) {
5722
+ linePath.lineTo(x, yFromPrice(second));
5723
+ }
5724
+ lastX = x;
5725
+ }
5726
+ if (started) {
5727
+ if (chartType === "area" || chartType === "baseline") {
5728
+ const baseY = chartType === "baseline" ? yFromPrice(mergedOptions.baselinePrice ?? (yMin + yMax) / 2) : chartBottom;
5729
+ const fillPath = new Path2D(linePath);
5730
+ fillPath.lineTo(lastX, baseY);
5731
+ fillPath.lineTo(firstX, baseY);
5732
+ fillPath.closePath();
5733
+ ctx.save();
5734
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
5735
+ ctx.fillStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5736
+ ctx.fill(fillPath);
5737
+ ctx.restore();
5738
+ }
5739
+ ctx.save();
5740
+ ctx.strokeStyle = chartType === "baseline" ? mergedOptions.upColor : mergedOptions.lineColor;
5741
+ ctx.lineWidth = seriesLineWidth;
5742
+ ctx.lineJoin = "round";
5743
+ ctx.stroke(linePath);
5744
+ ctx.restore();
5745
+ }
5746
+ } else if (isLineStyleChart) {
5496
5747
  const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
5497
5748
  const linePath = new Path2D();
5498
5749
  let started = false;
@@ -5658,6 +5909,105 @@ function createChart(element, options = {}) {
5658
5909
  ctx.stroke(downHollowBodies);
5659
5910
  }
5660
5911
  }
5912
+ const compareAxisTags = [];
5913
+ for (const plan of comparePlans) {
5914
+ const color = plan.state.options.color ?? "#f59e0b";
5915
+ const lineWidth = Math.max(1, plan.state.options.lineWidth ?? 1.5);
5916
+ const path = new Path2D();
5917
+ let started = false;
5918
+ let firstX = 0;
5919
+ let lastX = 0;
5920
+ let lastY = 0;
5921
+ let lastPrice = Number.NaN;
5922
+ const emit = (x, y) => {
5923
+ if (!started) {
5924
+ path.moveTo(x, y);
5925
+ firstX = x;
5926
+ started = true;
5927
+ } else {
5928
+ path.lineTo(x, y);
5929
+ }
5930
+ lastX = x;
5931
+ lastY = y;
5932
+ };
5933
+ const xForIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
5934
+ if (shouldDownsample) {
5935
+ let column = Number.NaN;
5936
+ let minValue = 0;
5937
+ let maxValue = 0;
5938
+ let minIndex = 0;
5939
+ let maxIndex = 0;
5940
+ let hasColumn = false;
5941
+ const flushColumn = () => {
5942
+ if (!hasColumn) return;
5943
+ const lowFirst = minIndex <= maxIndex;
5944
+ const first = plan.toPrice(lowFirst ? minValue : maxValue);
5945
+ const second = plan.toPrice(lowFirst ? maxValue : minValue);
5946
+ if (Number.isFinite(first)) emit(column, yFromPrice(first));
5947
+ if (Number.isFinite(second) && second !== first) emit(column, yFromPrice(second));
5948
+ lastPrice = plan.toPrice(maxIndex >= minIndex ? maxValue : minValue);
5949
+ };
5950
+ for (let index = startIndex; index <= endIndex; index += 1) {
5951
+ const value = plan.aligned[index];
5952
+ if (value === void 0 || !Number.isFinite(value)) continue;
5953
+ const nextColumn = Math.round(xForIndex(index));
5954
+ if (!hasColumn || nextColumn !== column) {
5955
+ flushColumn();
5956
+ column = nextColumn;
5957
+ minValue = value;
5958
+ maxValue = value;
5959
+ minIndex = index;
5960
+ maxIndex = index;
5961
+ hasColumn = true;
5962
+ continue;
5963
+ }
5964
+ if (value < minValue) {
5965
+ minValue = value;
5966
+ minIndex = index;
5967
+ }
5968
+ if (value > maxValue) {
5969
+ maxValue = value;
5970
+ maxIndex = index;
5971
+ }
5972
+ }
5973
+ flushColumn();
5974
+ } else {
5975
+ for (let index = startIndex; index <= endIndex; index += 1) {
5976
+ const value = plan.aligned[index];
5977
+ if (value === void 0 || !Number.isFinite(value)) continue;
5978
+ const price = plan.toPrice(value);
5979
+ if (!Number.isFinite(price)) continue;
5980
+ lastPrice = price;
5981
+ emit(xForIndex(index), yFromPrice(price));
5982
+ }
5983
+ }
5984
+ if (!started) continue;
5985
+ if (plan.state.options.style === "area") {
5986
+ const fillPath = new Path2D(path);
5987
+ fillPath.lineTo(lastX, chartBottom);
5988
+ fillPath.lineTo(firstX, chartBottom);
5989
+ fillPath.closePath();
5990
+ ctx.save();
5991
+ ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity)) * 0.6;
5992
+ ctx.fillStyle = color;
5993
+ ctx.fill(fillPath);
5994
+ ctx.restore();
5995
+ }
5996
+ ctx.save();
5997
+ ctx.strokeStyle = color;
5998
+ ctx.lineWidth = lineWidth;
5999
+ ctx.lineJoin = "round";
6000
+ ctx.stroke(path);
6001
+ ctx.restore();
6002
+ if (Number.isFinite(lastPrice)) {
6003
+ const label = plan.state.options.label ?? plan.state.options.id;
6004
+ const nativeLast = plan.lastNativeValue;
6005
+ const usePercentTag = plan.usePercent && plan.baseCompare > 0 && Number.isFinite(nativeLast);
6006
+ const changePct = usePercentTag ? (nativeLast / plan.baseCompare - 1) * 100 : 0;
6007
+ const shortText = usePercentTag ? `${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%` : formatPrice(nativeLast);
6008
+ compareAxisTags.push({ y: lastY, text: `${label} ${shortText}`, shortText, color });
6009
+ }
6010
+ }
5661
6011
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
5662
6012
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
5663
6013
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
@@ -6432,6 +6782,35 @@ function createChart(element, options = {}) {
6432
6782
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
6433
6783
  ctx.font = prevFont;
6434
6784
  }
6785
+ if (compareAxisTags.length > 0 && width - chartRight > 0) {
6786
+ const prevFont = ctx.font;
6787
+ const tagFontSize = Math.max(9, (resolvedAxis.fontSize ?? 12) - 2);
6788
+ ctx.font = `${tagFontSize}px ${mergedOptions.fontFamily}`;
6789
+ ctx.textBaseline = "middle";
6790
+ ctx.textAlign = "left";
6791
+ const placed = [];
6792
+ const tagHeight = tagFontSize + 6;
6793
+ const gutterWidth = width - chartRight - 4;
6794
+ for (const tag of [...compareAxisTags].sort((a, b) => a.y - b.y)) {
6795
+ const text = measureTextWidth(tag.text) + 10 <= gutterWidth ? tag.text : tag.shortText;
6796
+ const tagWidth = Math.min(gutterWidth, measureTextWidth(text) + 10);
6797
+ if (tagWidth <= 0) continue;
6798
+ let tagY = clamp(tag.y - tagHeight / 2, chartTop, chartBottom - tagHeight);
6799
+ for (const slot of placed) {
6800
+ if (tagY < slot.bottom + 2 && tagY + tagHeight > slot.top - 2) {
6801
+ tagY = slot.bottom + 2;
6802
+ }
6803
+ }
6804
+ placed.push({ top: tagY, bottom: tagY + tagHeight });
6805
+ ctx.save();
6806
+ ctx.fillStyle = tag.color;
6807
+ fillRoundedRect(chartRight + 2, Math.round(tagY), Math.round(tagWidth), tagHeight, 3);
6808
+ ctx.fillStyle = labelTextColorOn(tag.color, "#ffffff");
6809
+ ctx.fillText(text, chartRight + 7, Math.round(tagY + tagHeight / 2));
6810
+ ctx.restore();
6811
+ }
6812
+ ctx.font = prevFont;
6813
+ }
6435
6814
  if (baseCtx) {
6436
6815
  if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
6437
6816
  baseCanvas.width = canvas.width;
@@ -6455,6 +6834,7 @@ function createChart(element, options = {}) {
6455
6834
  xSpan
6456
6835
  };
6457
6836
  paintCrosshairOverlay();
6837
+ emitSelectionChangeIfMoved();
6458
6838
  };
6459
6839
  const paintCrosshairOverlay = () => {
6460
6840
  crosshairPriceActionRegion = null;
@@ -7055,6 +7435,98 @@ function createChart(element, options = {}) {
7055
7435
  const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7056
7436
  return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
7057
7437
  };
7438
+ const getDrawingBounds = (drawing) => {
7439
+ if (!drawState || drawing.points.length === 0) {
7440
+ return null;
7441
+ }
7442
+ const { chartLeft, chartRight, chartTop, chartBottom } = drawState;
7443
+ let left;
7444
+ let right;
7445
+ let top;
7446
+ let bottom;
7447
+ if (drawing.type === "horizontal-line") {
7448
+ const y = canvasYFromDrawingPrice(drawing.points[0].price);
7449
+ left = chartLeft;
7450
+ right = chartRight;
7451
+ top = y;
7452
+ bottom = y;
7453
+ } else if (drawing.type === "vertical-line") {
7454
+ const x = canvasXFromDrawingPoint(drawing.points[0]);
7455
+ left = x;
7456
+ right = x;
7457
+ top = chartTop;
7458
+ bottom = chartBottom;
7459
+ } else {
7460
+ const xs = drawing.points.map((point) => canvasXFromDrawingPoint(point));
7461
+ const ys = drawing.points.map((point) => canvasYFromDrawingPrice(point.price));
7462
+ left = Math.min(...xs);
7463
+ right = Math.max(...xs);
7464
+ top = Math.min(...ys);
7465
+ bottom = Math.max(...ys);
7466
+ if (drawing.type === "ray") {
7467
+ right = chartRight;
7468
+ }
7469
+ }
7470
+ const clampX = (value) => clamp(value, chartLeft, chartRight);
7471
+ const clampY = (value) => clamp(value, chartTop, chartBottom);
7472
+ left = clampX(left);
7473
+ right = clampX(right);
7474
+ top = clampY(top);
7475
+ bottom = clampY(bottom);
7476
+ return {
7477
+ left,
7478
+ top,
7479
+ right,
7480
+ bottom,
7481
+ centerX: (left + right) / 2,
7482
+ centerY: (top + bottom) / 2
7483
+ };
7484
+ };
7485
+ const getSelectedDrawingWithBounds = () => {
7486
+ if (!selectedDrawingId) {
7487
+ return null;
7488
+ }
7489
+ const drawing = drawings.find((entry) => entry.id === selectedDrawingId);
7490
+ if (!drawing || !drawing.visible) {
7491
+ return null;
7492
+ }
7493
+ const bounds = getDrawingBounds(drawing);
7494
+ return bounds ? { drawing: serializeDrawing(drawing), bounds } : null;
7495
+ };
7496
+ const emitSelectionChangeIfMoved = () => {
7497
+ if (!selectionChangeHandler) {
7498
+ lastSelectionSignature = null;
7499
+ return;
7500
+ }
7501
+ const selection = getSelectedDrawingWithBounds();
7502
+ if (!selection || !drawState) {
7503
+ if (lastSelectionSignature !== null) {
7504
+ lastSelectionSignature = null;
7505
+ selectionChangeHandler(null);
7506
+ }
7507
+ return;
7508
+ }
7509
+ const dragging = drawingDragState !== null;
7510
+ const { bounds } = selection;
7511
+ const signature = `${selection.drawing.id}:${Math.round(bounds.left)}:${Math.round(bounds.top)}:${Math.round(
7512
+ bounds.right
7513
+ )}:${Math.round(bounds.bottom)}:${dragging ? 1 : 0}`;
7514
+ if (signature === lastSelectionSignature) {
7515
+ return;
7516
+ }
7517
+ lastSelectionSignature = signature;
7518
+ selectionChangeHandler({
7519
+ drawing: selection.drawing,
7520
+ bounds,
7521
+ plot: {
7522
+ left: drawState.chartLeft,
7523
+ top: drawState.chartTop,
7524
+ right: drawState.chartRight,
7525
+ bottom: drawState.chartBottom
7526
+ },
7527
+ dragging
7528
+ });
7529
+ };
7058
7530
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
7059
7531
  const dx = x2 - x1;
7060
7532
  const dy = y2 - y1;
@@ -8021,6 +8493,10 @@ function createChart(element, options = {}) {
8021
8493
  if (event.pointerType === "touch" || event.pointerType === "pen") {
8022
8494
  event.preventDefault();
8023
8495
  }
8496
+ if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
8497
+ focusFromPointer = true;
8498
+ canvas.focus({ preventScroll: true });
8499
+ }
8024
8500
  cancelKineticPan();
8025
8501
  panVelocitySamples.length = 0;
8026
8502
  magnetModifierActive = event.metaKey || event.ctrlKey;
@@ -8138,12 +8614,14 @@ function createChart(element, options = {}) {
8138
8614
  const drawingHit = getDrawingHit(point.x, point.y);
8139
8615
  if (drawingHit) {
8140
8616
  selectedDrawingId = drawingHit.drawing.id;
8617
+ const selectBounds = getDrawingBounds(drawingHit.drawing);
8141
8618
  drawingSelectHandler?.({
8142
8619
  drawing: serializeDrawing(drawingHit.drawing),
8143
8620
  target: drawingHit.target,
8144
8621
  ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
8145
8622
  x: point.x,
8146
- y: point.y
8623
+ y: point.y,
8624
+ ...selectBounds ? { bounds: selectBounds } : {}
8147
8625
  });
8148
8626
  if (!drawingHit.drawing.locked) {
8149
8627
  const startCanvasPoint = drawingPointFromCanvas(point.x, point.y, false);
@@ -8704,7 +9182,152 @@ function createChart(element, options = {}) {
8704
9182
  };
8705
9183
  const onContextMenu = (event) => {
8706
9184
  event.preventDefault();
9185
+ if (!contextMenuHandler || !drawState) {
9186
+ return;
9187
+ }
9188
+ const point = getCanvasPoint(event);
9189
+ const hitRegion = getHitRegion(point.x, point.y);
9190
+ if (hitRegion === "outside") {
9191
+ return;
9192
+ }
9193
+ const pane = hitRegion === "plot" ? getPaneAt(point.x, point.y) : null;
9194
+ const drawingHit = hitRegion === "plot" && !pane ? getDrawingHit(point.x, point.y) : null;
9195
+ if (drawingHit) {
9196
+ if (selectedDrawingId !== drawingHit.drawing.id) {
9197
+ selectedDrawingId = drawingHit.drawing.id;
9198
+ scheduleDraw();
9199
+ }
9200
+ }
9201
+ const region = drawingHit ? "drawing" : pane ? "indicator-pane" : hitRegion;
9202
+ const index = indexFromCanvasX(point.x);
9203
+ const barTime = index === null ? null : getTimeForIndex(index);
9204
+ const bar = index === null ? void 0 : data[index];
9205
+ const inMainPane = !pane && (hitRegion === "plot" || hitRegion === "y-axis");
9206
+ contextMenuHandler({
9207
+ region,
9208
+ x: point.x,
9209
+ y: point.y,
9210
+ clientX: event.clientX,
9211
+ clientY: event.clientY,
9212
+ ...inMainPane ? { price: roundToPricePrecision(priceFromCanvasY(point.y)) } : {},
9213
+ ...index === null ? {} : { index },
9214
+ ...barTime ? { time: barTime.toISOString() } : {},
9215
+ ...bar ? {
9216
+ point: {
9217
+ t: bar.time.toISOString(),
9218
+ o: bar.o,
9219
+ h: bar.h,
9220
+ l: bar.l,
9221
+ c: bar.c,
9222
+ ...bar.v === void 0 ? {} : { v: bar.v }
9223
+ }
9224
+ } : {},
9225
+ ...drawingHit ? {
9226
+ drawing: serializeDrawing(drawingHit.drawing),
9227
+ drawingTarget: drawingHit.target,
9228
+ ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex }
9229
+ } : {},
9230
+ ...pane ? { indicator: { id: pane.id, type: pane.type } } : {}
9231
+ });
9232
+ };
9233
+ const onCanvasKeyDown = (event) => {
9234
+ const keyboard = mergedOptions.keyboard ?? {};
9235
+ if (keyboard.enabled === false) {
9236
+ return;
9237
+ }
9238
+ if (event.altKey || event.ctrlKey || event.metaKey) {
9239
+ return;
9240
+ }
9241
+ const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
9242
+ const emit = (action, drawing) => {
9243
+ keyboardShortcutHandler?.({
9244
+ action,
9245
+ key: event.key,
9246
+ shiftKey: event.shiftKey,
9247
+ ...drawing ? { drawing } : {}
9248
+ });
9249
+ };
9250
+ switch (event.key) {
9251
+ case "Delete":
9252
+ case "Backspace": {
9253
+ if (keyboard.deleteSelectedDrawing === false || !selectedDrawingId) {
9254
+ return;
9255
+ }
9256
+ const target = drawings.find((entry) => entry.id === selectedDrawingId);
9257
+ if (!target) {
9258
+ return;
9259
+ }
9260
+ const removed = serializeDrawing(target);
9261
+ removeDrawing(selectedDrawingId);
9262
+ selectedDrawingId = null;
9263
+ scheduleDraw();
9264
+ emit("delete-drawing", removed);
9265
+ break;
9266
+ }
9267
+ case "Escape": {
9268
+ if (cancelDrawing()) {
9269
+ emit("cancel");
9270
+ break;
9271
+ }
9272
+ if (activeDrawingTool) {
9273
+ setActiveDrawingTool(null);
9274
+ emit("cancel");
9275
+ break;
9276
+ }
9277
+ if (selectedDrawingId) {
9278
+ setSelectedDrawing(null);
9279
+ emit("cancel");
9280
+ break;
9281
+ }
9282
+ return;
9283
+ }
9284
+ case "ArrowLeft":
9285
+ panX(-panStep);
9286
+ emit("pan-left");
9287
+ break;
9288
+ case "ArrowRight":
9289
+ panX(panStep);
9290
+ emit("pan-right");
9291
+ break;
9292
+ case "ArrowUp":
9293
+ case "ArrowDown": {
9294
+ if (!drawState) {
9295
+ return;
9296
+ }
9297
+ const step = (drawState.yMax - drawState.yMin) / 20 * (event.shiftKey ? 5 : 1);
9298
+ panY(event.key === "ArrowUp" ? step : -step);
9299
+ emit(event.key === "ArrowUp" ? "pan-up" : "pan-down");
9300
+ break;
9301
+ }
9302
+ case "+":
9303
+ case "=":
9304
+ zoomInX();
9305
+ emit("zoom-in");
9306
+ break;
9307
+ case "-":
9308
+ case "_":
9309
+ zoomOutX();
9310
+ emit("zoom-out");
9311
+ break;
9312
+ case "Home":
9313
+ fitContent();
9314
+ emit("reset-viewport");
9315
+ break;
9316
+ case "End":
9317
+ setFollowingLatest(true);
9318
+ emit("scroll-to-realtime");
9319
+ break;
9320
+ case "r":
9321
+ case "R":
9322
+ resetViewport();
9323
+ emit("reset-viewport");
9324
+ break;
9325
+ default:
9326
+ return;
9327
+ }
9328
+ event.preventDefault();
8707
9329
  };
9330
+ canvas.addEventListener("keydown", onCanvasKeyDown);
8708
9331
  canvas.addEventListener("pointerdown", onPointerDown);
8709
9332
  canvas.addEventListener("pointermove", onPointerMove);
8710
9333
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -8751,6 +9374,7 @@ function createChart(element, options = {}) {
8751
9374
  resetRightMarginCache();
8752
9375
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
8753
9376
  doubleClickAction = mergedOptions.doubleClickAction;
9377
+ applyAccessibilityAttributes();
8754
9378
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
8755
9379
  if (!isTickerSmoothingEnabled) {
8756
9380
  smoothedTickerPrice = null;
@@ -9134,6 +9758,23 @@ function createChart(element, options = {}) {
9134
9758
  const onDrawingHover = (handler) => {
9135
9759
  drawingHoverHandler = handler;
9136
9760
  };
9761
+ const onSelectionChange = (handler) => {
9762
+ selectionChangeHandler = handler;
9763
+ lastSelectionSignature = null;
9764
+ if (handler) {
9765
+ emitSelectionChangeIfMoved();
9766
+ }
9767
+ };
9768
+ const getSelectedDrawing = () => getSelectedDrawingWithBounds();
9769
+ const onContextMenuHandler = (handler) => {
9770
+ contextMenuHandler = handler;
9771
+ };
9772
+ const onKeyboardShortcut = (handler) => {
9773
+ keyboardShortcutHandler = handler;
9774
+ };
9775
+ const focus = () => {
9776
+ canvas.focus({ preventScroll: true });
9777
+ };
9137
9778
  const saveState = () => {
9138
9779
  const drawingDefaults = {};
9139
9780
  for (const [tool, defaults] of drawingToolDefaults) {
@@ -9198,6 +9839,7 @@ function createChart(element, options = {}) {
9198
9839
  drawRafId = null;
9199
9840
  pendingDrawUpdateAutoScale = false;
9200
9841
  }
9842
+ canvas.removeEventListener("keydown", onCanvasKeyDown);
9201
9843
  canvas.removeEventListener("pointerdown", onPointerDown);
9202
9844
  canvas.removeEventListener("pointermove", onPointerMove);
9203
9845
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -9261,10 +9903,19 @@ function createChart(element, options = {}) {
9261
9903
  onDrawingSelect,
9262
9904
  onDrawingDoubleClick,
9263
9905
  onDrawingEditText,
9906
+ onSelectionChange,
9907
+ getSelectedDrawing,
9908
+ onContextMenu: onContextMenuHandler,
9909
+ onKeyboardShortcut,
9910
+ focus,
9264
9911
  setMagnetMode,
9265
9912
  getMagnetMode,
9266
9913
  setPriceScale,
9267
9914
  getPriceScale,
9915
+ setCompareSeries,
9916
+ addCompareSeries,
9917
+ removeCompareSeries,
9918
+ getCompareSeries,
9268
9919
  cancelDrawing,
9269
9920
  setTradeMarkers,
9270
9921
  setSelectedDrawing,