hyperprop-charting-library 0.1.136 → 0.1.138

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.
@@ -2121,6 +2121,82 @@ var hashScriptSource = (source) => {
2121
2121
  }
2122
2122
  return (hash >>> 0).toString(36);
2123
2123
  };
2124
+ var SCRIPT_SOURCE_INPUT_VALUES = ["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];
2125
+ var slugifyInputLabel = (label) => {
2126
+ const slug = String(label).trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2127
+ return slug || "input";
2128
+ };
2129
+ var createScriptInputHelper = (values, registry) => {
2130
+ const usedKeys = /* @__PURE__ */ new Set();
2131
+ const resolveKey = (label, options) => {
2132
+ let key = options?.key ?? slugifyInputLabel(label);
2133
+ if (usedKeys.has(key)) {
2134
+ let suffix = 2;
2135
+ while (usedKeys.has(`${key}_${suffix}`)) suffix += 1;
2136
+ key = `${key}_${suffix}`;
2137
+ }
2138
+ usedKeys.add(key);
2139
+ return key;
2140
+ };
2141
+ const resolve = (key, fallback, coerce) => {
2142
+ if (!values) return fallback;
2143
+ const raw = values[key];
2144
+ if (raw === void 0 || raw === null) return fallback;
2145
+ const coerced = coerce(raw);
2146
+ return coerced === void 0 ? fallback : coerced;
2147
+ };
2148
+ const coerceNumber = (raw) => {
2149
+ const num = typeof raw === "number" ? raw : Number(raw);
2150
+ return Number.isFinite(num) ? num : void 0;
2151
+ };
2152
+ const numberInput = (integer) => (label, defaultValue, options) => {
2153
+ const key = resolveKey(label, options);
2154
+ registry?.push({
2155
+ key,
2156
+ label,
2157
+ type: "number",
2158
+ default: defaultValue,
2159
+ ...options?.min !== void 0 ? { min: options.min } : {},
2160
+ ...options?.max !== void 0 ? { max: options.max } : {},
2161
+ ...options?.step !== void 0 ? { step: options.step } : integer ? { step: 1 } : {}
2162
+ });
2163
+ const value = resolve(key, defaultValue, coerceNumber);
2164
+ return integer ? Math.round(value) : value;
2165
+ };
2166
+ const selectInput = (label, defaultValue, choices, options) => {
2167
+ const key = resolveKey(label, options);
2168
+ const normalized = (Array.isArray(choices) ? choices : []).map(
2169
+ (choice) => typeof choice === "string" ? { value: choice, label: choice } : { value: String(choice.value), label: String(choice.label ?? choice.value) }
2170
+ );
2171
+ registry?.push({ key, label, type: "select", default: defaultValue, options: normalized });
2172
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : String(raw));
2173
+ };
2174
+ return Object.freeze({
2175
+ int: numberInput(true),
2176
+ float: numberInput(false),
2177
+ bool: (label, defaultValue = false, options) => {
2178
+ const key = resolveKey(label, options);
2179
+ registry?.push({ key, label, type: "boolean", default: defaultValue });
2180
+ return resolve(
2181
+ key,
2182
+ defaultValue,
2183
+ (raw) => typeof raw === "boolean" ? raw : raw === "true" ? true : raw === "false" ? false : void 0
2184
+ );
2185
+ },
2186
+ color: (label, defaultValue, options) => {
2187
+ const key = resolveKey(label, options);
2188
+ registry?.push({ key, label, type: "color", default: defaultValue });
2189
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : void 0);
2190
+ },
2191
+ string: (label, defaultValue = "", options) => {
2192
+ const key = resolveKey(label, options);
2193
+ registry?.push({ key, label, type: "string", default: defaultValue });
2194
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : String(raw));
2195
+ },
2196
+ select: selectInput,
2197
+ source: (label = "Source", defaultValue = "close", options) => selectInput(label, defaultValue, SCRIPT_SOURCE_INPUT_VALUES, options)
2198
+ });
2199
+ };
2124
2200
  var SCRIPT_GUARD_BUDGET_MS = 1e3;
2125
2201
  var ScriptBudgetError = class extends Error {
2126
2202
  constructor() {
@@ -2395,31 +2471,34 @@ var injectLoopGuards = (source, guardName) => {
2395
2471
  }
2396
2472
  return out;
2397
2473
  };
2398
- var compileScriptCompute = (source, guard) => {
2474
+ var compileScriptFactory = (source) => {
2399
2475
  const makeFactory = (body) => new Function(
2400
2476
  "__hpGuard",
2477
+ "input",
2401
2478
  `"use strict";
2402
2479
  ${body}
2403
2480
  ;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
2404
2481
  return compute;`
2405
2482
  );
2406
- let factory;
2407
2483
  try {
2408
- factory = makeFactory(injectLoopGuards(source, "__hpGuard"));
2484
+ return makeFactory(injectLoopGuards(source, "__hpGuard"));
2409
2485
  } catch {
2410
- factory = makeFactory(source);
2486
+ return makeFactory(source);
2411
2487
  }
2412
- guard.reset();
2413
- return factory(guard);
2414
2488
  };
2415
- var validateScriptSource = (source) => {
2489
+ var extractScriptInputs = (source) => {
2490
+ const registry = [];
2416
2491
  try {
2417
- compileScriptCompute(source, createScriptGuard());
2418
- return null;
2492
+ const factory = compileScriptFactory(source);
2493
+ const guard = createScriptGuard();
2494
+ guard.reset();
2495
+ factory(guard, createScriptInputHelper(null, registry));
2496
+ return { inputs: registry, error: null };
2419
2497
  } catch (error) {
2420
- return error instanceof Error ? error.message : String(error);
2498
+ return { inputs: registry, error: error instanceof Error ? error.message : String(error) };
2421
2499
  }
2422
2500
  };
2501
+ var validateScriptSource = (source) => extractScriptInputs(source).error;
2423
2502
  var drawScriptError = (ctx, renderContext, name, message) => {
2424
2503
  ctx.save();
2425
2504
  ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
@@ -2436,18 +2515,24 @@ var compileScriptIndicator = (definition) => {
2436
2515
  for (const input of definition.inputs ?? []) {
2437
2516
  defaultInputs[input.key] = input.default;
2438
2517
  }
2439
- let compiled = null;
2518
+ let factory = null;
2440
2519
  let compileError = null;
2441
2520
  const guard = createScriptGuard();
2442
2521
  try {
2443
- compiled = compileScriptCompute(definition.source, guard);
2522
+ factory = compileScriptFactory(definition.source);
2523
+ const declared = [];
2524
+ guard.reset();
2525
+ factory(guard, createScriptInputHelper(null, declared));
2526
+ for (const input of declared) {
2527
+ defaultInputs[input.key] = input.default;
2528
+ }
2444
2529
  } catch (error) {
2445
2530
  compileError = error instanceof Error ? error.message : String(error);
2446
2531
  }
2447
2532
  const sourceKey = hashScriptSource(definition.source);
2448
2533
  let trippedError = null;
2449
2534
  const runCompute = (data, inputs) => {
2450
- if (!compiled) {
2535
+ if (!factory || compileError) {
2451
2536
  return { error: compileError ?? "Script failed to compile" };
2452
2537
  }
2453
2538
  if (trippedError) {
@@ -2455,6 +2540,7 @@ var compileScriptIndicator = (definition) => {
2455
2540
  }
2456
2541
  try {
2457
2542
  guard.reset();
2543
+ const compiled = factory(guard, createScriptInputHelper(inputs, null));
2458
2544
  const result = compiled(data, inputs, SCRIPT_HELPERS);
2459
2545
  if (!result || !Array.isArray(result.plots)) {
2460
2546
  return { error: "compute() must return { plots: [...] }" };
@@ -2733,6 +2819,8 @@ var DEFAULT_OPTIONS = {
2733
2819
  pinOutOfRangeLines: false,
2734
2820
  doubleClickEnabled: true,
2735
2821
  doubleClickAction: "reset",
2822
+ keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2823
+ accessibility: { label: "Price chart", description: "", focusable: true },
2736
2824
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2737
2825
  grid: DEFAULT_GRID_OPTIONS,
2738
2826
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2775,6 +2863,14 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2775
2863
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
2776
2864
  ...options.yAxis ?? {}
2777
2865
  },
2866
+ keyboard: {
2867
+ ...baseOptions.keyboard,
2868
+ ...options.keyboard ?? {}
2869
+ },
2870
+ accessibility: {
2871
+ ...baseOptions.accessibility,
2872
+ ...options.accessibility ?? {}
2873
+ },
2778
2874
  crosshair: {
2779
2875
  ...baseOptions.crosshair,
2780
2876
  ...options.crosshair ?? {}
@@ -2948,7 +3044,29 @@ function createChart(element, options = {}) {
2948
3044
  let drawingSelectHandler = null;
2949
3045
  let drawingDoubleClickHandler = null;
2950
3046
  let drawingEditTextHandler = null;
3047
+ let selectionChangeHandler = null;
3048
+ let contextMenuHandler = null;
3049
+ let keyboardShortcutHandler = null;
3050
+ let lastSelectionSignature = null;
2951
3051
  let magnetMode = "none";
3052
+ let priceScaleMode = "linear";
3053
+ let priceScaleInverted = false;
3054
+ const PRICE_SCALE_LOG_FLOOR = 1e-9;
3055
+ const priceScaleTransform = (price) => priceScaleMode === "log" ? Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, price)) : price;
3056
+ const priceScaleUntransform = (value) => priceScaleMode === "log" ? Math.pow(10, value) : value;
3057
+ const percentBaselineForXStart = (xStartValue) => {
3058
+ if (priceScaleMode !== "percent") return null;
3059
+ const bar = data[clamp(Math.round(xStartValue), 0, Math.max(0, data.length - 1))];
3060
+ return bar && bar.c > 0 ? bar.c : null;
3061
+ };
3062
+ const formatPriceForScale = (price, percentBaseline) => {
3063
+ if (percentBaseline !== null) {
3064
+ const pct = (price / percentBaseline - 1) * 100;
3065
+ const rounded = Math.abs(pct) < 5e-3 ? 0 : pct;
3066
+ return `${rounded > 0 ? "+" : ""}${rounded.toFixed(2)}%`;
3067
+ }
3068
+ return formatPrice(price);
3069
+ };
2952
3070
  let magnetModifierActive = false;
2953
3071
  let shiftKeyActive = false;
2954
3072
  const MAGNET_WEAK_THRESHOLD_PX = 16;
@@ -3092,6 +3210,39 @@ function createChart(element, options = {}) {
3092
3210
  canvas.style.setProperty("-webkit-user-select", "none");
3093
3211
  canvas.style.setProperty("-webkit-touch-callout", "none");
3094
3212
  canvas.setAttribute("draggable", "false");
3213
+ const applyAccessibilityAttributes = () => {
3214
+ const a11y = mergedOptions.accessibility ?? {};
3215
+ const focusable = a11y.focusable !== false;
3216
+ if (focusable) {
3217
+ canvas.tabIndex = 0;
3218
+ } else {
3219
+ canvas.removeAttribute("tabindex");
3220
+ }
3221
+ canvas.setAttribute("role", "application");
3222
+ canvas.setAttribute("aria-label", a11y.label ?? "Price chart");
3223
+ if (a11y.description) {
3224
+ canvas.setAttribute("aria-description", a11y.description);
3225
+ } else {
3226
+ canvas.removeAttribute("aria-description");
3227
+ }
3228
+ if (focusable && (mergedOptions.keyboard?.enabled ?? true)) {
3229
+ canvas.setAttribute(
3230
+ "aria-keyshortcuts",
3231
+ "ArrowLeft ArrowRight ArrowUp ArrowDown Plus Minus Home End Delete Escape"
3232
+ );
3233
+ } else {
3234
+ canvas.removeAttribute("aria-keyshortcuts");
3235
+ }
3236
+ };
3237
+ applyAccessibilityAttributes();
3238
+ let focusFromPointer = false;
3239
+ canvas.addEventListener("focus", () => {
3240
+ canvas.style.outline = focusFromPointer ? "none" : "";
3241
+ focusFromPointer = false;
3242
+ });
3243
+ canvas.addEventListener("blur", () => {
3244
+ canvas.style.outline = "none";
3245
+ });
3095
3246
  element.innerHTML = "";
3096
3247
  element.appendChild(canvas);
3097
3248
  const baseCanvas = document.createElement("canvas");
@@ -4282,9 +4433,14 @@ function createChart(element, options = {}) {
4282
4433
  yMax
4283
4434
  };
4284
4435
  plotBottomForHit = fullChartBottom;
4436
+ const scaledYMin = priceScaleTransform(yMin);
4437
+ const scaledYRange = priceScaleTransform(yMax) - scaledYMin || 1;
4285
4438
  const yFromPrice = (price) => {
4286
- return chartBottom - (price - yMin) / yRange * chartHeight;
4439
+ const ratio = (priceScaleTransform(price) - scaledYMin) / scaledYRange;
4440
+ return priceScaleInverted ? chartTop + ratio * chartHeight : chartBottom - ratio * chartHeight;
4287
4441
  };
4442
+ const percentBaseline = percentBaselineForXStart(xStart);
4443
+ const formatAxisPrice = (price) => formatPriceForScale(price, percentBaseline);
4288
4444
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
4289
4445
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
4290
4446
  const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
@@ -5191,19 +5347,40 @@ function createChart(element, options = {}) {
5191
5347
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
5192
5348
  const gridColor = grid.color ?? mergedOptions.gridColor;
5193
5349
  const priceTicks = [];
5194
- {
5195
- const targetStep = yRange / Math.max(1, yTicks);
5196
- let step;
5197
- if (targetStep > 0 && Number.isFinite(targetStep)) {
5198
- const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5199
- const frac = targetStep / base;
5200
- step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5201
- const instrumentTick = getConfiguredTickSize();
5202
- if (instrumentTick > 0 && step < instrumentTick * 1e6) {
5203
- step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5350
+ const pickNiceStep = (targetStep) => {
5351
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return 1;
5352
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5353
+ const frac = targetStep / base;
5354
+ return (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5355
+ };
5356
+ if (percentBaseline !== null) {
5357
+ const pctMin = (yMin / percentBaseline - 1) * 100;
5358
+ const pctMax = (yMax / percentBaseline - 1) * 100;
5359
+ const step = pickNiceStep((pctMax - pctMin) / Math.max(1, yTicks));
5360
+ const firstTick = Math.ceil(pctMin / step) * step;
5361
+ for (let i = 0; ; i += 1) {
5362
+ const pct = firstTick + i * step;
5363
+ if (pct > pctMax + step * 1e-6) break;
5364
+ priceTicks.push(percentBaseline * (1 + pct / 100));
5365
+ if (priceTicks.length > 200) break;
5366
+ }
5367
+ } else if (priceScaleMode === "log" && yMin > 0 && yMax / yMin > 4) {
5368
+ const mantissas = Math.log10(yMax / yMin) > 8 ? [1] : [1, 2, 5];
5369
+ const firstDecade = Math.floor(Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, yMin)));
5370
+ const lastDecade = Math.ceil(Math.log10(yMax));
5371
+ for (let decade = firstDecade; decade <= lastDecade; decade += 1) {
5372
+ for (const mantissa of mantissas) {
5373
+ const price = mantissa * Math.pow(10, decade);
5374
+ if (price >= yMin && price <= yMax) priceTicks.push(price);
5375
+ if (priceTicks.length > 200) break;
5204
5376
  }
5205
- } else {
5206
- step = 1;
5377
+ }
5378
+ } else {
5379
+ const targetStep = yRange / Math.max(1, yTicks);
5380
+ let step = pickNiceStep(targetStep);
5381
+ const instrumentTick = getConfiguredTickSize();
5382
+ if (targetStep > 0 && Number.isFinite(targetStep) && instrumentTick > 0 && step < instrumentTick * 1e6) {
5383
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5207
5384
  }
5208
5385
  const firstTick = Math.ceil(yMin / step) * step;
5209
5386
  for (let i = 0; ; i += 1) {
@@ -5966,7 +6143,7 @@ function createChart(element, options = {}) {
5966
6143
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
5967
6144
  for (const price of priceTicks) {
5968
6145
  const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
5969
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6146
+ drawText(formatAxisPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
5970
6147
  }
5971
6148
  ctx.font = prevFont;
5972
6149
  }
@@ -6041,7 +6218,7 @@ function createChart(element, options = {}) {
6041
6218
  ...ticker.showCountdownInLabel ? [getCountdownText()] : []
6042
6219
  ].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
6043
6220
  addPriceAxisLabel({
6044
- text: formatPrice(tickerPrice),
6221
+ text: formatAxisPrice(tickerPrice),
6045
6222
  ...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
6046
6223
  subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
6047
6224
  ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
@@ -6068,7 +6245,7 @@ function createChart(element, options = {}) {
6068
6245
  const previousClose = previousCloseCandidate;
6069
6246
  drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
6070
6247
  addPriceAxisLabel({
6071
- text: `PDC ${formatPrice(previousClose)}`,
6248
+ text: `PDC ${formatAxisPrice(previousClose)}`,
6072
6249
  price: previousClose,
6073
6250
  backgroundColor: labels.backgroundColor,
6074
6251
  textColor: labels.mutedTextColor,
@@ -6087,7 +6264,7 @@ function createChart(element, options = {}) {
6087
6264
  if (point.l < visibleLow) visibleLow = point.l;
6088
6265
  }
6089
6266
  addPriceAxisLabel({
6090
- text: `H ${formatPrice(visibleHigh)}`,
6267
+ text: `H ${formatAxisPrice(visibleHigh)}`,
6091
6268
  price: visibleHigh,
6092
6269
  backgroundColor: labels.backgroundColor,
6093
6270
  textColor: labels.textColor,
@@ -6095,7 +6272,7 @@ function createChart(element, options = {}) {
6095
6272
  priority: 40
6096
6273
  });
6097
6274
  addPriceAxisLabel({
6098
- text: `L ${formatPrice(visibleLow)}`,
6275
+ text: `L ${formatAxisPrice(visibleLow)}`,
6099
6276
  price: visibleLow,
6100
6277
  backgroundColor: labels.backgroundColor,
6101
6278
  textColor: labels.textColor,
@@ -6107,7 +6284,7 @@ function createChart(element, options = {}) {
6107
6284
  if (Number.isFinite(labels.bidPrice)) {
6108
6285
  drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
6109
6286
  addPriceAxisLabel({
6110
- text: `B ${formatPrice(labels.bidPrice)}`,
6287
+ text: `B ${formatAxisPrice(labels.bidPrice)}`,
6111
6288
  price: labels.bidPrice,
6112
6289
  backgroundColor: labels.bidColor,
6113
6290
  textColor: "#0b1220",
@@ -6118,7 +6295,7 @@ function createChart(element, options = {}) {
6118
6295
  if (Number.isFinite(labels.askPrice)) {
6119
6296
  drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
6120
6297
  addPriceAxisLabel({
6121
- text: `A ${formatPrice(labels.askPrice)}`,
6298
+ text: `A ${formatAxisPrice(labels.askPrice)}`,
6122
6299
  price: labels.askPrice,
6123
6300
  backgroundColor: labels.askColor,
6124
6301
  textColor: "#0b1220",
@@ -6293,6 +6470,7 @@ function createChart(element, options = {}) {
6293
6470
  xSpan
6294
6471
  };
6295
6472
  paintCrosshairOverlay();
6473
+ emitSelectionChangeIfMoved();
6296
6474
  };
6297
6475
  const paintCrosshairOverlay = () => {
6298
6476
  crosshairPriceActionRegion = null;
@@ -6379,8 +6557,11 @@ function createChart(element, options = {}) {
6379
6557
  ctx.restore();
6380
6558
  };
6381
6559
  if (crosshair.showPriceLabel) {
6382
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
6383
- const priceText = formatPrice(hoverPrice);
6560
+ const scaledYMin = priceScaleTransform(yMin);
6561
+ const scaledYRange = priceScaleTransform(yMin + yRange) - scaledYMin || 1;
6562
+ const hoverRatio = priceScaleInverted ? (cy - chartTop) / chartHeight : (chartBottom - cy) / chartHeight;
6563
+ const hoverPrice = priceScaleUntransform(scaledYMin + hoverRatio * scaledYRange);
6564
+ const priceText = formatPriceForScale(hoverPrice, percentBaselineForXStart(xStart));
6384
6565
  const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
6385
6566
  const priceX = getRightAxisLabelX(chartRight);
6386
6567
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
@@ -6548,8 +6729,8 @@ function createChart(element, options = {}) {
6548
6729
  const maxRange = bounds.hardSpan;
6549
6730
  const nextRange = clamp(currentRange * factor, minRange, maxRange);
6550
6731
  const anchorRatio = clamp((anchorY - drawState.chartTop) / drawState.chartHeight, 0, 1);
6551
- const anchorPrice = currentMax - anchorRatio * currentRange;
6552
- const nextMax = anchorPrice + anchorRatio * nextRange;
6732
+ const anchorPrice = priceScaleInverted ? currentMin + anchorRatio * currentRange : currentMax - anchorRatio * currentRange;
6733
+ const nextMax = priceScaleInverted ? anchorPrice + (1 - anchorRatio) * nextRange : anchorPrice + anchorRatio * nextRange;
6553
6734
  const nextMin = nextMax - nextRange;
6554
6735
  const clamped = clampYRange(nextMin, nextMax);
6555
6736
  yMinOverride = clamped.min;
@@ -6573,7 +6754,7 @@ function createChart(element, options = {}) {
6573
6754
  const currentMin = yMinOverride ?? drawState.yMin;
6574
6755
  const currentMax = yMaxOverride ?? drawState.yMax;
6575
6756
  const range = currentMax - currentMin || 1;
6576
- const priceShift = deltaY / drawState.chartHeight * range;
6757
+ const priceShift = deltaY / drawState.chartHeight * range * (priceScaleInverted ? -1 : 1);
6577
6758
  const clamped = clampYRange(currentMin + priceShift, currentMax + priceShift);
6578
6759
  yMinOverride = clamped.min;
6579
6760
  yMaxOverride = clamped.max;
@@ -6806,8 +6987,14 @@ function createChart(element, options = {}) {
6806
6987
  if (!drawState) {
6807
6988
  return 0;
6808
6989
  }
6809
- const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
6810
- return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
6990
+ const ratio = clamp(
6991
+ priceScaleInverted ? (y - drawState.chartTop) / drawState.chartHeight : (drawState.chartBottom - y) / drawState.chartHeight,
6992
+ 0,
6993
+ 1
6994
+ );
6995
+ const scaledMin = priceScaleTransform(drawState.yMin);
6996
+ const scaledMax = priceScaleTransform(drawState.yMax);
6997
+ return priceScaleUntransform(scaledMin + ratio * (scaledMax - scaledMin));
6811
6998
  };
6812
6999
  const applyMagnet = (y, index, price) => {
6813
7000
  const effective = magnetModifierActive ? "strong" : magnetMode;
@@ -6879,8 +7066,102 @@ function createChart(element, options = {}) {
6879
7066
  };
6880
7067
  const canvasYFromDrawingPrice = (price) => {
6881
7068
  if (!drawState) return 0;
6882
- const range = drawState.yMax - drawState.yMin || 1;
6883
- return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
7069
+ const scaledMin = priceScaleTransform(drawState.yMin);
7070
+ const scaledRange = priceScaleTransform(drawState.yMax) - scaledMin || 1;
7071
+ const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7072
+ return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
7073
+ };
7074
+ const getDrawingBounds = (drawing) => {
7075
+ if (!drawState || drawing.points.length === 0) {
7076
+ return null;
7077
+ }
7078
+ const { chartLeft, chartRight, chartTop, chartBottom } = drawState;
7079
+ let left;
7080
+ let right;
7081
+ let top;
7082
+ let bottom;
7083
+ if (drawing.type === "horizontal-line") {
7084
+ const y = canvasYFromDrawingPrice(drawing.points[0].price);
7085
+ left = chartLeft;
7086
+ right = chartRight;
7087
+ top = y;
7088
+ bottom = y;
7089
+ } else if (drawing.type === "vertical-line") {
7090
+ const x = canvasXFromDrawingPoint(drawing.points[0]);
7091
+ left = x;
7092
+ right = x;
7093
+ top = chartTop;
7094
+ bottom = chartBottom;
7095
+ } else {
7096
+ const xs = drawing.points.map((point) => canvasXFromDrawingPoint(point));
7097
+ const ys = drawing.points.map((point) => canvasYFromDrawingPrice(point.price));
7098
+ left = Math.min(...xs);
7099
+ right = Math.max(...xs);
7100
+ top = Math.min(...ys);
7101
+ bottom = Math.max(...ys);
7102
+ if (drawing.type === "ray") {
7103
+ right = chartRight;
7104
+ }
7105
+ }
7106
+ const clampX = (value) => clamp(value, chartLeft, chartRight);
7107
+ const clampY = (value) => clamp(value, chartTop, chartBottom);
7108
+ left = clampX(left);
7109
+ right = clampX(right);
7110
+ top = clampY(top);
7111
+ bottom = clampY(bottom);
7112
+ return {
7113
+ left,
7114
+ top,
7115
+ right,
7116
+ bottom,
7117
+ centerX: (left + right) / 2,
7118
+ centerY: (top + bottom) / 2
7119
+ };
7120
+ };
7121
+ const getSelectedDrawingWithBounds = () => {
7122
+ if (!selectedDrawingId) {
7123
+ return null;
7124
+ }
7125
+ const drawing = drawings.find((entry) => entry.id === selectedDrawingId);
7126
+ if (!drawing || !drawing.visible) {
7127
+ return null;
7128
+ }
7129
+ const bounds = getDrawingBounds(drawing);
7130
+ return bounds ? { drawing: serializeDrawing(drawing), bounds } : null;
7131
+ };
7132
+ const emitSelectionChangeIfMoved = () => {
7133
+ if (!selectionChangeHandler) {
7134
+ lastSelectionSignature = null;
7135
+ return;
7136
+ }
7137
+ const selection = getSelectedDrawingWithBounds();
7138
+ if (!selection || !drawState) {
7139
+ if (lastSelectionSignature !== null) {
7140
+ lastSelectionSignature = null;
7141
+ selectionChangeHandler(null);
7142
+ }
7143
+ return;
7144
+ }
7145
+ const dragging = drawingDragState !== null;
7146
+ const { bounds } = selection;
7147
+ const signature = `${selection.drawing.id}:${Math.round(bounds.left)}:${Math.round(bounds.top)}:${Math.round(
7148
+ bounds.right
7149
+ )}:${Math.round(bounds.bottom)}:${dragging ? 1 : 0}`;
7150
+ if (signature === lastSelectionSignature) {
7151
+ return;
7152
+ }
7153
+ lastSelectionSignature = signature;
7154
+ selectionChangeHandler({
7155
+ drawing: selection.drawing,
7156
+ bounds,
7157
+ plot: {
7158
+ left: drawState.chartLeft,
7159
+ top: drawState.chartTop,
7160
+ right: drawState.chartRight,
7161
+ bottom: drawState.chartBottom
7162
+ },
7163
+ dragging
7164
+ });
6884
7165
  };
6885
7166
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
6886
7167
  const dx = x2 - x1;
@@ -7848,6 +8129,10 @@ function createChart(element, options = {}) {
7848
8129
  if (event.pointerType === "touch" || event.pointerType === "pen") {
7849
8130
  event.preventDefault();
7850
8131
  }
8132
+ if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
8133
+ focusFromPointer = true;
8134
+ canvas.focus({ preventScroll: true });
8135
+ }
7851
8136
  cancelKineticPan();
7852
8137
  panVelocitySamples.length = 0;
7853
8138
  magnetModifierActive = event.metaKey || event.ctrlKey;
@@ -7965,12 +8250,14 @@ function createChart(element, options = {}) {
7965
8250
  const drawingHit = getDrawingHit(point.x, point.y);
7966
8251
  if (drawingHit) {
7967
8252
  selectedDrawingId = drawingHit.drawing.id;
8253
+ const selectBounds = getDrawingBounds(drawingHit.drawing);
7968
8254
  drawingSelectHandler?.({
7969
8255
  drawing: serializeDrawing(drawingHit.drawing),
7970
8256
  target: drawingHit.target,
7971
8257
  ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
7972
8258
  x: point.x,
7973
- y: point.y
8259
+ y: point.y,
8260
+ ...selectBounds ? { bounds: selectBounds } : {}
7974
8261
  });
7975
8262
  if (!drawingHit.drawing.locked) {
7976
8263
  const startCanvasPoint = drawingPointFromCanvas(point.x, point.y, false);
@@ -8531,7 +8818,152 @@ function createChart(element, options = {}) {
8531
8818
  };
8532
8819
  const onContextMenu = (event) => {
8533
8820
  event.preventDefault();
8821
+ if (!contextMenuHandler || !drawState) {
8822
+ return;
8823
+ }
8824
+ const point = getCanvasPoint(event);
8825
+ const hitRegion = getHitRegion(point.x, point.y);
8826
+ if (hitRegion === "outside") {
8827
+ return;
8828
+ }
8829
+ const pane = hitRegion === "plot" ? getPaneAt(point.x, point.y) : null;
8830
+ const drawingHit = hitRegion === "plot" && !pane ? getDrawingHit(point.x, point.y) : null;
8831
+ if (drawingHit) {
8832
+ if (selectedDrawingId !== drawingHit.drawing.id) {
8833
+ selectedDrawingId = drawingHit.drawing.id;
8834
+ scheduleDraw();
8835
+ }
8836
+ }
8837
+ const region = drawingHit ? "drawing" : pane ? "indicator-pane" : hitRegion;
8838
+ const index = indexFromCanvasX(point.x);
8839
+ const barTime = index === null ? null : getTimeForIndex(index);
8840
+ const bar = index === null ? void 0 : data[index];
8841
+ const inMainPane = !pane && (hitRegion === "plot" || hitRegion === "y-axis");
8842
+ contextMenuHandler({
8843
+ region,
8844
+ x: point.x,
8845
+ y: point.y,
8846
+ clientX: event.clientX,
8847
+ clientY: event.clientY,
8848
+ ...inMainPane ? { price: roundToPricePrecision(priceFromCanvasY(point.y)) } : {},
8849
+ ...index === null ? {} : { index },
8850
+ ...barTime ? { time: barTime.toISOString() } : {},
8851
+ ...bar ? {
8852
+ point: {
8853
+ t: bar.time.toISOString(),
8854
+ o: bar.o,
8855
+ h: bar.h,
8856
+ l: bar.l,
8857
+ c: bar.c,
8858
+ ...bar.v === void 0 ? {} : { v: bar.v }
8859
+ }
8860
+ } : {},
8861
+ ...drawingHit ? {
8862
+ drawing: serializeDrawing(drawingHit.drawing),
8863
+ drawingTarget: drawingHit.target,
8864
+ ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex }
8865
+ } : {},
8866
+ ...pane ? { indicator: { id: pane.id, type: pane.type } } : {}
8867
+ });
8534
8868
  };
8869
+ const onCanvasKeyDown = (event) => {
8870
+ const keyboard = mergedOptions.keyboard ?? {};
8871
+ if (keyboard.enabled === false) {
8872
+ return;
8873
+ }
8874
+ if (event.altKey || event.ctrlKey || event.metaKey) {
8875
+ return;
8876
+ }
8877
+ const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
8878
+ const emit = (action, drawing) => {
8879
+ keyboardShortcutHandler?.({
8880
+ action,
8881
+ key: event.key,
8882
+ shiftKey: event.shiftKey,
8883
+ ...drawing ? { drawing } : {}
8884
+ });
8885
+ };
8886
+ switch (event.key) {
8887
+ case "Delete":
8888
+ case "Backspace": {
8889
+ if (keyboard.deleteSelectedDrawing === false || !selectedDrawingId) {
8890
+ return;
8891
+ }
8892
+ const target = drawings.find((entry) => entry.id === selectedDrawingId);
8893
+ if (!target) {
8894
+ return;
8895
+ }
8896
+ const removed = serializeDrawing(target);
8897
+ removeDrawing(selectedDrawingId);
8898
+ selectedDrawingId = null;
8899
+ scheduleDraw();
8900
+ emit("delete-drawing", removed);
8901
+ break;
8902
+ }
8903
+ case "Escape": {
8904
+ if (cancelDrawing()) {
8905
+ emit("cancel");
8906
+ break;
8907
+ }
8908
+ if (activeDrawingTool) {
8909
+ setActiveDrawingTool(null);
8910
+ emit("cancel");
8911
+ break;
8912
+ }
8913
+ if (selectedDrawingId) {
8914
+ setSelectedDrawing(null);
8915
+ emit("cancel");
8916
+ break;
8917
+ }
8918
+ return;
8919
+ }
8920
+ case "ArrowLeft":
8921
+ panX(-panStep);
8922
+ emit("pan-left");
8923
+ break;
8924
+ case "ArrowRight":
8925
+ panX(panStep);
8926
+ emit("pan-right");
8927
+ break;
8928
+ case "ArrowUp":
8929
+ case "ArrowDown": {
8930
+ if (!drawState) {
8931
+ return;
8932
+ }
8933
+ const step = (drawState.yMax - drawState.yMin) / 20 * (event.shiftKey ? 5 : 1);
8934
+ panY(event.key === "ArrowUp" ? step : -step);
8935
+ emit(event.key === "ArrowUp" ? "pan-up" : "pan-down");
8936
+ break;
8937
+ }
8938
+ case "+":
8939
+ case "=":
8940
+ zoomInX();
8941
+ emit("zoom-in");
8942
+ break;
8943
+ case "-":
8944
+ case "_":
8945
+ zoomOutX();
8946
+ emit("zoom-out");
8947
+ break;
8948
+ case "Home":
8949
+ fitContent();
8950
+ emit("reset-viewport");
8951
+ break;
8952
+ case "End":
8953
+ setFollowingLatest(true);
8954
+ emit("scroll-to-realtime");
8955
+ break;
8956
+ case "r":
8957
+ case "R":
8958
+ resetViewport();
8959
+ emit("reset-viewport");
8960
+ break;
8961
+ default:
8962
+ return;
8963
+ }
8964
+ event.preventDefault();
8965
+ };
8966
+ canvas.addEventListener("keydown", onCanvasKeyDown);
8535
8967
  canvas.addEventListener("pointerdown", onPointerDown);
8536
8968
  canvas.addEventListener("pointermove", onPointerMove);
8537
8969
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -8578,6 +9010,7 @@ function createChart(element, options = {}) {
8578
9010
  resetRightMarginCache();
8579
9011
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
8580
9012
  doubleClickAction = mergedOptions.doubleClickAction;
9013
+ applyAccessibilityAttributes();
8581
9014
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
8582
9015
  if (!isTickerSmoothingEnabled) {
8583
9016
  smoothedTickerPrice = null;
@@ -8894,6 +9327,16 @@ function createChart(element, options = {}) {
8894
9327
  magnetMode = mode;
8895
9328
  };
8896
9329
  const getMagnetMode = () => magnetMode;
9330
+ const setPriceScale = (options2) => {
9331
+ if (options2.mode === "linear" || options2.mode === "log" || options2.mode === "percent") {
9332
+ priceScaleMode = options2.mode;
9333
+ }
9334
+ if (typeof options2.inverted === "boolean") {
9335
+ priceScaleInverted = options2.inverted;
9336
+ }
9337
+ scheduleDraw();
9338
+ };
9339
+ const getPriceScale = () => ({ mode: priceScaleMode, inverted: priceScaleInverted });
8897
9340
  const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
8898
9341
  const setDrawings = (nextDrawings) => {
8899
9342
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
@@ -8951,6 +9394,23 @@ function createChart(element, options = {}) {
8951
9394
  const onDrawingHover = (handler) => {
8952
9395
  drawingHoverHandler = handler;
8953
9396
  };
9397
+ const onSelectionChange = (handler) => {
9398
+ selectionChangeHandler = handler;
9399
+ lastSelectionSignature = null;
9400
+ if (handler) {
9401
+ emitSelectionChangeIfMoved();
9402
+ }
9403
+ };
9404
+ const getSelectedDrawing = () => getSelectedDrawingWithBounds();
9405
+ const onContextMenuHandler = (handler) => {
9406
+ contextMenuHandler = handler;
9407
+ };
9408
+ const onKeyboardShortcut = (handler) => {
9409
+ keyboardShortcutHandler = handler;
9410
+ };
9411
+ const focus = () => {
9412
+ canvas.focus({ preventScroll: true });
9413
+ };
8954
9414
  const saveState = () => {
8955
9415
  const drawingDefaults = {};
8956
9416
  for (const [tool, defaults] of drawingToolDefaults) {
@@ -8963,6 +9423,7 @@ function createChart(element, options = {}) {
8963
9423
  drawings: getDrawings(),
8964
9424
  indicators: getIndicators(),
8965
9425
  magnetMode: getMagnetMode(),
9426
+ priceScale: getPriceScale(),
8966
9427
  drawingDefaults
8967
9428
  };
8968
9429
  };
@@ -8982,6 +9443,9 @@ function createChart(element, options = {}) {
8982
9443
  if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
8983
9444
  setMagnetMode(state.magnetMode);
8984
9445
  }
9446
+ if (state.priceScale && typeof state.priceScale === "object") {
9447
+ setPriceScale(state.priceScale);
9448
+ }
8985
9449
  if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
8986
9450
  for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
8987
9451
  if (defaults && typeof defaults === "object") {
@@ -9011,6 +9475,7 @@ function createChart(element, options = {}) {
9011
9475
  drawRafId = null;
9012
9476
  pendingDrawUpdateAutoScale = false;
9013
9477
  }
9478
+ canvas.removeEventListener("keydown", onCanvasKeyDown);
9014
9479
  canvas.removeEventListener("pointerdown", onPointerDown);
9015
9480
  canvas.removeEventListener("pointermove", onPointerMove);
9016
9481
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -9074,8 +9539,15 @@ function createChart(element, options = {}) {
9074
9539
  onDrawingSelect,
9075
9540
  onDrawingDoubleClick,
9076
9541
  onDrawingEditText,
9542
+ onSelectionChange,
9543
+ getSelectedDrawing,
9544
+ onContextMenu: onContextMenuHandler,
9545
+ onKeyboardShortcut,
9546
+ focus,
9077
9547
  setMagnetMode,
9078
9548
  getMagnetMode,
9549
+ setPriceScale,
9550
+ getPriceScale,
9079
9551
  cancelDrawing,
9080
9552
  setTradeMarkers,
9081
9553
  setSelectedDrawing,
@@ -9101,7 +9573,9 @@ function createChart(element, options = {}) {
9101
9573
  export {
9102
9574
  FIB_DEFAULT_PALETTE,
9103
9575
  POSITION_DEFAULT_COLORS,
9576
+ SCRIPT_SOURCE_INPUT_VALUES,
9104
9577
  compileScriptIndicator,
9105
9578
  createChart,
9579
+ extractScriptInputs,
9106
9580
  validateScriptSource
9107
9581
  };