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.
@@ -22,8 +22,10 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  FIB_DEFAULT_PALETTE: () => FIB_DEFAULT_PALETTE,
24
24
  POSITION_DEFAULT_COLORS: () => POSITION_DEFAULT_COLORS,
25
+ SCRIPT_SOURCE_INPUT_VALUES: () => SCRIPT_SOURCE_INPUT_VALUES,
25
26
  compileScriptIndicator: () => compileScriptIndicator,
26
27
  createChart: () => createChart,
28
+ extractScriptInputs: () => extractScriptInputs,
27
29
  validateScriptSource: () => validateScriptSource
28
30
  });
29
31
  module.exports = __toCommonJS(index_exports);
@@ -2151,6 +2153,82 @@ var hashScriptSource = (source) => {
2151
2153
  }
2152
2154
  return (hash >>> 0).toString(36);
2153
2155
  };
2156
+ var SCRIPT_SOURCE_INPUT_VALUES = ["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];
2157
+ var slugifyInputLabel = (label) => {
2158
+ const slug = String(label).trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2159
+ return slug || "input";
2160
+ };
2161
+ var createScriptInputHelper = (values, registry) => {
2162
+ const usedKeys = /* @__PURE__ */ new Set();
2163
+ const resolveKey = (label, options) => {
2164
+ let key = options?.key ?? slugifyInputLabel(label);
2165
+ if (usedKeys.has(key)) {
2166
+ let suffix = 2;
2167
+ while (usedKeys.has(`${key}_${suffix}`)) suffix += 1;
2168
+ key = `${key}_${suffix}`;
2169
+ }
2170
+ usedKeys.add(key);
2171
+ return key;
2172
+ };
2173
+ const resolve = (key, fallback, coerce) => {
2174
+ if (!values) return fallback;
2175
+ const raw = values[key];
2176
+ if (raw === void 0 || raw === null) return fallback;
2177
+ const coerced = coerce(raw);
2178
+ return coerced === void 0 ? fallback : coerced;
2179
+ };
2180
+ const coerceNumber = (raw) => {
2181
+ const num = typeof raw === "number" ? raw : Number(raw);
2182
+ return Number.isFinite(num) ? num : void 0;
2183
+ };
2184
+ const numberInput = (integer) => (label, defaultValue, options) => {
2185
+ const key = resolveKey(label, options);
2186
+ registry?.push({
2187
+ key,
2188
+ label,
2189
+ type: "number",
2190
+ default: defaultValue,
2191
+ ...options?.min !== void 0 ? { min: options.min } : {},
2192
+ ...options?.max !== void 0 ? { max: options.max } : {},
2193
+ ...options?.step !== void 0 ? { step: options.step } : integer ? { step: 1 } : {}
2194
+ });
2195
+ const value = resolve(key, defaultValue, coerceNumber);
2196
+ return integer ? Math.round(value) : value;
2197
+ };
2198
+ const selectInput = (label, defaultValue, choices, options) => {
2199
+ const key = resolveKey(label, options);
2200
+ const normalized = (Array.isArray(choices) ? choices : []).map(
2201
+ (choice) => typeof choice === "string" ? { value: choice, label: choice } : { value: String(choice.value), label: String(choice.label ?? choice.value) }
2202
+ );
2203
+ registry?.push({ key, label, type: "select", default: defaultValue, options: normalized });
2204
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : String(raw));
2205
+ };
2206
+ return Object.freeze({
2207
+ int: numberInput(true),
2208
+ float: numberInput(false),
2209
+ bool: (label, defaultValue = false, options) => {
2210
+ const key = resolveKey(label, options);
2211
+ registry?.push({ key, label, type: "boolean", default: defaultValue });
2212
+ return resolve(
2213
+ key,
2214
+ defaultValue,
2215
+ (raw) => typeof raw === "boolean" ? raw : raw === "true" ? true : raw === "false" ? false : void 0
2216
+ );
2217
+ },
2218
+ color: (label, defaultValue, options) => {
2219
+ const key = resolveKey(label, options);
2220
+ registry?.push({ key, label, type: "color", default: defaultValue });
2221
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : void 0);
2222
+ },
2223
+ string: (label, defaultValue = "", options) => {
2224
+ const key = resolveKey(label, options);
2225
+ registry?.push({ key, label, type: "string", default: defaultValue });
2226
+ return resolve(key, defaultValue, (raw) => typeof raw === "string" ? raw : String(raw));
2227
+ },
2228
+ select: selectInput,
2229
+ source: (label = "Source", defaultValue = "close", options) => selectInput(label, defaultValue, SCRIPT_SOURCE_INPUT_VALUES, options)
2230
+ });
2231
+ };
2154
2232
  var SCRIPT_GUARD_BUDGET_MS = 1e3;
2155
2233
  var ScriptBudgetError = class extends Error {
2156
2234
  constructor() {
@@ -2425,31 +2503,34 @@ var injectLoopGuards = (source, guardName) => {
2425
2503
  }
2426
2504
  return out;
2427
2505
  };
2428
- var compileScriptCompute = (source, guard) => {
2506
+ var compileScriptFactory = (source) => {
2429
2507
  const makeFactory = (body) => new Function(
2430
2508
  "__hpGuard",
2509
+ "input",
2431
2510
  `"use strict";
2432
2511
  ${body}
2433
2512
  ;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
2434
2513
  return compute;`
2435
2514
  );
2436
- let factory;
2437
2515
  try {
2438
- factory = makeFactory(injectLoopGuards(source, "__hpGuard"));
2516
+ return makeFactory(injectLoopGuards(source, "__hpGuard"));
2439
2517
  } catch {
2440
- factory = makeFactory(source);
2518
+ return makeFactory(source);
2441
2519
  }
2442
- guard.reset();
2443
- return factory(guard);
2444
2520
  };
2445
- var validateScriptSource = (source) => {
2521
+ var extractScriptInputs = (source) => {
2522
+ const registry = [];
2446
2523
  try {
2447
- compileScriptCompute(source, createScriptGuard());
2448
- return null;
2524
+ const factory = compileScriptFactory(source);
2525
+ const guard = createScriptGuard();
2526
+ guard.reset();
2527
+ factory(guard, createScriptInputHelper(null, registry));
2528
+ return { inputs: registry, error: null };
2449
2529
  } catch (error) {
2450
- return error instanceof Error ? error.message : String(error);
2530
+ return { inputs: registry, error: error instanceof Error ? error.message : String(error) };
2451
2531
  }
2452
2532
  };
2533
+ var validateScriptSource = (source) => extractScriptInputs(source).error;
2453
2534
  var drawScriptError = (ctx, renderContext, name, message) => {
2454
2535
  ctx.save();
2455
2536
  ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
@@ -2466,18 +2547,24 @@ var compileScriptIndicator = (definition) => {
2466
2547
  for (const input of definition.inputs ?? []) {
2467
2548
  defaultInputs[input.key] = input.default;
2468
2549
  }
2469
- let compiled = null;
2550
+ let factory = null;
2470
2551
  let compileError = null;
2471
2552
  const guard = createScriptGuard();
2472
2553
  try {
2473
- compiled = compileScriptCompute(definition.source, guard);
2554
+ factory = compileScriptFactory(definition.source);
2555
+ const declared = [];
2556
+ guard.reset();
2557
+ factory(guard, createScriptInputHelper(null, declared));
2558
+ for (const input of declared) {
2559
+ defaultInputs[input.key] = input.default;
2560
+ }
2474
2561
  } catch (error) {
2475
2562
  compileError = error instanceof Error ? error.message : String(error);
2476
2563
  }
2477
2564
  const sourceKey = hashScriptSource(definition.source);
2478
2565
  let trippedError = null;
2479
2566
  const runCompute = (data, inputs) => {
2480
- if (!compiled) {
2567
+ if (!factory || compileError) {
2481
2568
  return { error: compileError ?? "Script failed to compile" };
2482
2569
  }
2483
2570
  if (trippedError) {
@@ -2485,6 +2572,7 @@ var compileScriptIndicator = (definition) => {
2485
2572
  }
2486
2573
  try {
2487
2574
  guard.reset();
2575
+ const compiled = factory(guard, createScriptInputHelper(inputs, null));
2488
2576
  const result = compiled(data, inputs, SCRIPT_HELPERS);
2489
2577
  if (!result || !Array.isArray(result.plots)) {
2490
2578
  return { error: "compute() must return { plots: [...] }" };
@@ -2763,6 +2851,8 @@ var DEFAULT_OPTIONS = {
2763
2851
  pinOutOfRangeLines: false,
2764
2852
  doubleClickEnabled: true,
2765
2853
  doubleClickAction: "reset",
2854
+ keyboard: { enabled: true, panBars: 1, deleteSelectedDrawing: true },
2855
+ accessibility: { label: "Price chart", description: "", focusable: true },
2766
2856
  crosshair: DEFAULT_CROSSHAIR_OPTIONS,
2767
2857
  grid: DEFAULT_GRID_OPTIONS,
2768
2858
  watermark: DEFAULT_WATERMARK_OPTIONS,
@@ -2805,6 +2895,14 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
2805
2895
  ...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
2806
2896
  ...options.yAxis ?? {}
2807
2897
  },
2898
+ keyboard: {
2899
+ ...baseOptions.keyboard,
2900
+ ...options.keyboard ?? {}
2901
+ },
2902
+ accessibility: {
2903
+ ...baseOptions.accessibility,
2904
+ ...options.accessibility ?? {}
2905
+ },
2808
2906
  crosshair: {
2809
2907
  ...baseOptions.crosshair,
2810
2908
  ...options.crosshair ?? {}
@@ -2978,7 +3076,29 @@ function createChart(element, options = {}) {
2978
3076
  let drawingSelectHandler = null;
2979
3077
  let drawingDoubleClickHandler = null;
2980
3078
  let drawingEditTextHandler = null;
3079
+ let selectionChangeHandler = null;
3080
+ let contextMenuHandler = null;
3081
+ let keyboardShortcutHandler = null;
3082
+ let lastSelectionSignature = null;
2981
3083
  let magnetMode = "none";
3084
+ let priceScaleMode = "linear";
3085
+ let priceScaleInverted = false;
3086
+ const PRICE_SCALE_LOG_FLOOR = 1e-9;
3087
+ const priceScaleTransform = (price) => priceScaleMode === "log" ? Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, price)) : price;
3088
+ const priceScaleUntransform = (value) => priceScaleMode === "log" ? Math.pow(10, value) : value;
3089
+ const percentBaselineForXStart = (xStartValue) => {
3090
+ if (priceScaleMode !== "percent") return null;
3091
+ const bar = data[clamp(Math.round(xStartValue), 0, Math.max(0, data.length - 1))];
3092
+ return bar && bar.c > 0 ? bar.c : null;
3093
+ };
3094
+ const formatPriceForScale = (price, percentBaseline) => {
3095
+ if (percentBaseline !== null) {
3096
+ const pct = (price / percentBaseline - 1) * 100;
3097
+ const rounded = Math.abs(pct) < 5e-3 ? 0 : pct;
3098
+ return `${rounded > 0 ? "+" : ""}${rounded.toFixed(2)}%`;
3099
+ }
3100
+ return formatPrice(price);
3101
+ };
2982
3102
  let magnetModifierActive = false;
2983
3103
  let shiftKeyActive = false;
2984
3104
  const MAGNET_WEAK_THRESHOLD_PX = 16;
@@ -3122,6 +3242,39 @@ function createChart(element, options = {}) {
3122
3242
  canvas.style.setProperty("-webkit-user-select", "none");
3123
3243
  canvas.style.setProperty("-webkit-touch-callout", "none");
3124
3244
  canvas.setAttribute("draggable", "false");
3245
+ const applyAccessibilityAttributes = () => {
3246
+ const a11y = mergedOptions.accessibility ?? {};
3247
+ const focusable = a11y.focusable !== false;
3248
+ if (focusable) {
3249
+ canvas.tabIndex = 0;
3250
+ } else {
3251
+ canvas.removeAttribute("tabindex");
3252
+ }
3253
+ canvas.setAttribute("role", "application");
3254
+ canvas.setAttribute("aria-label", a11y.label ?? "Price chart");
3255
+ if (a11y.description) {
3256
+ canvas.setAttribute("aria-description", a11y.description);
3257
+ } else {
3258
+ canvas.removeAttribute("aria-description");
3259
+ }
3260
+ if (focusable && (mergedOptions.keyboard?.enabled ?? true)) {
3261
+ canvas.setAttribute(
3262
+ "aria-keyshortcuts",
3263
+ "ArrowLeft ArrowRight ArrowUp ArrowDown Plus Minus Home End Delete Escape"
3264
+ );
3265
+ } else {
3266
+ canvas.removeAttribute("aria-keyshortcuts");
3267
+ }
3268
+ };
3269
+ applyAccessibilityAttributes();
3270
+ let focusFromPointer = false;
3271
+ canvas.addEventListener("focus", () => {
3272
+ canvas.style.outline = focusFromPointer ? "none" : "";
3273
+ focusFromPointer = false;
3274
+ });
3275
+ canvas.addEventListener("blur", () => {
3276
+ canvas.style.outline = "none";
3277
+ });
3125
3278
  element.innerHTML = "";
3126
3279
  element.appendChild(canvas);
3127
3280
  const baseCanvas = document.createElement("canvas");
@@ -4312,9 +4465,14 @@ function createChart(element, options = {}) {
4312
4465
  yMax
4313
4466
  };
4314
4467
  plotBottomForHit = fullChartBottom;
4468
+ const scaledYMin = priceScaleTransform(yMin);
4469
+ const scaledYRange = priceScaleTransform(yMax) - scaledYMin || 1;
4315
4470
  const yFromPrice = (price) => {
4316
- return chartBottom - (price - yMin) / yRange * chartHeight;
4471
+ const ratio = (priceScaleTransform(price) - scaledYMin) / scaledYRange;
4472
+ return priceScaleInverted ? chartTop + ratio * chartHeight : chartBottom - ratio * chartHeight;
4317
4473
  };
4474
+ const percentBaseline = percentBaselineForXStart(xStart);
4475
+ const formatAxisPrice = (price) => formatPriceForScale(price, percentBaseline);
4318
4476
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
4319
4477
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
4320
4478
  const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
@@ -5221,19 +5379,40 @@ function createChart(element, options = {}) {
5221
5379
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
5222
5380
  const gridColor = grid.color ?? mergedOptions.gridColor;
5223
5381
  const priceTicks = [];
5224
- {
5225
- const targetStep = yRange / Math.max(1, yTicks);
5226
- let step;
5227
- if (targetStep > 0 && Number.isFinite(targetStep)) {
5228
- const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5229
- const frac = targetStep / base;
5230
- step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5231
- const instrumentTick = getConfiguredTickSize();
5232
- if (instrumentTick > 0 && step < instrumentTick * 1e6) {
5233
- step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5382
+ const pickNiceStep = (targetStep) => {
5383
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return 1;
5384
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5385
+ const frac = targetStep / base;
5386
+ return (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5387
+ };
5388
+ if (percentBaseline !== null) {
5389
+ const pctMin = (yMin / percentBaseline - 1) * 100;
5390
+ const pctMax = (yMax / percentBaseline - 1) * 100;
5391
+ const step = pickNiceStep((pctMax - pctMin) / Math.max(1, yTicks));
5392
+ const firstTick = Math.ceil(pctMin / step) * step;
5393
+ for (let i = 0; ; i += 1) {
5394
+ const pct = firstTick + i * step;
5395
+ if (pct > pctMax + step * 1e-6) break;
5396
+ priceTicks.push(percentBaseline * (1 + pct / 100));
5397
+ if (priceTicks.length > 200) break;
5398
+ }
5399
+ } else if (priceScaleMode === "log" && yMin > 0 && yMax / yMin > 4) {
5400
+ const mantissas = Math.log10(yMax / yMin) > 8 ? [1] : [1, 2, 5];
5401
+ const firstDecade = Math.floor(Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, yMin)));
5402
+ const lastDecade = Math.ceil(Math.log10(yMax));
5403
+ for (let decade = firstDecade; decade <= lastDecade; decade += 1) {
5404
+ for (const mantissa of mantissas) {
5405
+ const price = mantissa * Math.pow(10, decade);
5406
+ if (price >= yMin && price <= yMax) priceTicks.push(price);
5407
+ if (priceTicks.length > 200) break;
5234
5408
  }
5235
- } else {
5236
- step = 1;
5409
+ }
5410
+ } else {
5411
+ const targetStep = yRange / Math.max(1, yTicks);
5412
+ let step = pickNiceStep(targetStep);
5413
+ const instrumentTick = getConfiguredTickSize();
5414
+ if (targetStep > 0 && Number.isFinite(targetStep) && instrumentTick > 0 && step < instrumentTick * 1e6) {
5415
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5237
5416
  }
5238
5417
  const firstTick = Math.ceil(yMin / step) * step;
5239
5418
  for (let i = 0; ; i += 1) {
@@ -5996,7 +6175,7 @@ function createChart(element, options = {}) {
5996
6175
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
5997
6176
  for (const price of priceTicks) {
5998
6177
  const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
5999
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6178
+ drawText(formatAxisPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6000
6179
  }
6001
6180
  ctx.font = prevFont;
6002
6181
  }
@@ -6071,7 +6250,7 @@ function createChart(element, options = {}) {
6071
6250
  ...ticker.showCountdownInLabel ? [getCountdownText()] : []
6072
6251
  ].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
6073
6252
  addPriceAxisLabel({
6074
- text: formatPrice(tickerPrice),
6253
+ text: formatAxisPrice(tickerPrice),
6075
6254
  ...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
6076
6255
  subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
6077
6256
  ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
@@ -6098,7 +6277,7 @@ function createChart(element, options = {}) {
6098
6277
  const previousClose = previousCloseCandidate;
6099
6278
  drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
6100
6279
  addPriceAxisLabel({
6101
- text: `PDC ${formatPrice(previousClose)}`,
6280
+ text: `PDC ${formatAxisPrice(previousClose)}`,
6102
6281
  price: previousClose,
6103
6282
  backgroundColor: labels.backgroundColor,
6104
6283
  textColor: labels.mutedTextColor,
@@ -6117,7 +6296,7 @@ function createChart(element, options = {}) {
6117
6296
  if (point.l < visibleLow) visibleLow = point.l;
6118
6297
  }
6119
6298
  addPriceAxisLabel({
6120
- text: `H ${formatPrice(visibleHigh)}`,
6299
+ text: `H ${formatAxisPrice(visibleHigh)}`,
6121
6300
  price: visibleHigh,
6122
6301
  backgroundColor: labels.backgroundColor,
6123
6302
  textColor: labels.textColor,
@@ -6125,7 +6304,7 @@ function createChart(element, options = {}) {
6125
6304
  priority: 40
6126
6305
  });
6127
6306
  addPriceAxisLabel({
6128
- text: `L ${formatPrice(visibleLow)}`,
6307
+ text: `L ${formatAxisPrice(visibleLow)}`,
6129
6308
  price: visibleLow,
6130
6309
  backgroundColor: labels.backgroundColor,
6131
6310
  textColor: labels.textColor,
@@ -6137,7 +6316,7 @@ function createChart(element, options = {}) {
6137
6316
  if (Number.isFinite(labels.bidPrice)) {
6138
6317
  drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
6139
6318
  addPriceAxisLabel({
6140
- text: `B ${formatPrice(labels.bidPrice)}`,
6319
+ text: `B ${formatAxisPrice(labels.bidPrice)}`,
6141
6320
  price: labels.bidPrice,
6142
6321
  backgroundColor: labels.bidColor,
6143
6322
  textColor: "#0b1220",
@@ -6148,7 +6327,7 @@ function createChart(element, options = {}) {
6148
6327
  if (Number.isFinite(labels.askPrice)) {
6149
6328
  drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
6150
6329
  addPriceAxisLabel({
6151
- text: `A ${formatPrice(labels.askPrice)}`,
6330
+ text: `A ${formatAxisPrice(labels.askPrice)}`,
6152
6331
  price: labels.askPrice,
6153
6332
  backgroundColor: labels.askColor,
6154
6333
  textColor: "#0b1220",
@@ -6323,6 +6502,7 @@ function createChart(element, options = {}) {
6323
6502
  xSpan
6324
6503
  };
6325
6504
  paintCrosshairOverlay();
6505
+ emitSelectionChangeIfMoved();
6326
6506
  };
6327
6507
  const paintCrosshairOverlay = () => {
6328
6508
  crosshairPriceActionRegion = null;
@@ -6409,8 +6589,11 @@ function createChart(element, options = {}) {
6409
6589
  ctx.restore();
6410
6590
  };
6411
6591
  if (crosshair.showPriceLabel) {
6412
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
6413
- const priceText = formatPrice(hoverPrice);
6592
+ const scaledYMin = priceScaleTransform(yMin);
6593
+ const scaledYRange = priceScaleTransform(yMin + yRange) - scaledYMin || 1;
6594
+ const hoverRatio = priceScaleInverted ? (cy - chartTop) / chartHeight : (chartBottom - cy) / chartHeight;
6595
+ const hoverPrice = priceScaleUntransform(scaledYMin + hoverRatio * scaledYRange);
6596
+ const priceText = formatPriceForScale(hoverPrice, percentBaselineForXStart(xStart));
6414
6597
  const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
6415
6598
  const priceX = getRightAxisLabelX(chartRight);
6416
6599
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
@@ -6578,8 +6761,8 @@ function createChart(element, options = {}) {
6578
6761
  const maxRange = bounds.hardSpan;
6579
6762
  const nextRange = clamp(currentRange * factor, minRange, maxRange);
6580
6763
  const anchorRatio = clamp((anchorY - drawState.chartTop) / drawState.chartHeight, 0, 1);
6581
- const anchorPrice = currentMax - anchorRatio * currentRange;
6582
- const nextMax = anchorPrice + anchorRatio * nextRange;
6764
+ const anchorPrice = priceScaleInverted ? currentMin + anchorRatio * currentRange : currentMax - anchorRatio * currentRange;
6765
+ const nextMax = priceScaleInverted ? anchorPrice + (1 - anchorRatio) * nextRange : anchorPrice + anchorRatio * nextRange;
6583
6766
  const nextMin = nextMax - nextRange;
6584
6767
  const clamped = clampYRange(nextMin, nextMax);
6585
6768
  yMinOverride = clamped.min;
@@ -6603,7 +6786,7 @@ function createChart(element, options = {}) {
6603
6786
  const currentMin = yMinOverride ?? drawState.yMin;
6604
6787
  const currentMax = yMaxOverride ?? drawState.yMax;
6605
6788
  const range = currentMax - currentMin || 1;
6606
- const priceShift = deltaY / drawState.chartHeight * range;
6789
+ const priceShift = deltaY / drawState.chartHeight * range * (priceScaleInverted ? -1 : 1);
6607
6790
  const clamped = clampYRange(currentMin + priceShift, currentMax + priceShift);
6608
6791
  yMinOverride = clamped.min;
6609
6792
  yMaxOverride = clamped.max;
@@ -6836,8 +7019,14 @@ function createChart(element, options = {}) {
6836
7019
  if (!drawState) {
6837
7020
  return 0;
6838
7021
  }
6839
- const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
6840
- return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
7022
+ const ratio = clamp(
7023
+ priceScaleInverted ? (y - drawState.chartTop) / drawState.chartHeight : (drawState.chartBottom - y) / drawState.chartHeight,
7024
+ 0,
7025
+ 1
7026
+ );
7027
+ const scaledMin = priceScaleTransform(drawState.yMin);
7028
+ const scaledMax = priceScaleTransform(drawState.yMax);
7029
+ return priceScaleUntransform(scaledMin + ratio * (scaledMax - scaledMin));
6841
7030
  };
6842
7031
  const applyMagnet = (y, index, price) => {
6843
7032
  const effective = magnetModifierActive ? "strong" : magnetMode;
@@ -6909,8 +7098,102 @@ function createChart(element, options = {}) {
6909
7098
  };
6910
7099
  const canvasYFromDrawingPrice = (price) => {
6911
7100
  if (!drawState) return 0;
6912
- const range = drawState.yMax - drawState.yMin || 1;
6913
- return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
7101
+ const scaledMin = priceScaleTransform(drawState.yMin);
7102
+ const scaledRange = priceScaleTransform(drawState.yMax) - scaledMin || 1;
7103
+ const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7104
+ return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
7105
+ };
7106
+ const getDrawingBounds = (drawing) => {
7107
+ if (!drawState || drawing.points.length === 0) {
7108
+ return null;
7109
+ }
7110
+ const { chartLeft, chartRight, chartTop, chartBottom } = drawState;
7111
+ let left;
7112
+ let right;
7113
+ let top;
7114
+ let bottom;
7115
+ if (drawing.type === "horizontal-line") {
7116
+ const y = canvasYFromDrawingPrice(drawing.points[0].price);
7117
+ left = chartLeft;
7118
+ right = chartRight;
7119
+ top = y;
7120
+ bottom = y;
7121
+ } else if (drawing.type === "vertical-line") {
7122
+ const x = canvasXFromDrawingPoint(drawing.points[0]);
7123
+ left = x;
7124
+ right = x;
7125
+ top = chartTop;
7126
+ bottom = chartBottom;
7127
+ } else {
7128
+ const xs = drawing.points.map((point) => canvasXFromDrawingPoint(point));
7129
+ const ys = drawing.points.map((point) => canvasYFromDrawingPrice(point.price));
7130
+ left = Math.min(...xs);
7131
+ right = Math.max(...xs);
7132
+ top = Math.min(...ys);
7133
+ bottom = Math.max(...ys);
7134
+ if (drawing.type === "ray") {
7135
+ right = chartRight;
7136
+ }
7137
+ }
7138
+ const clampX = (value) => clamp(value, chartLeft, chartRight);
7139
+ const clampY = (value) => clamp(value, chartTop, chartBottom);
7140
+ left = clampX(left);
7141
+ right = clampX(right);
7142
+ top = clampY(top);
7143
+ bottom = clampY(bottom);
7144
+ return {
7145
+ left,
7146
+ top,
7147
+ right,
7148
+ bottom,
7149
+ centerX: (left + right) / 2,
7150
+ centerY: (top + bottom) / 2
7151
+ };
7152
+ };
7153
+ const getSelectedDrawingWithBounds = () => {
7154
+ if (!selectedDrawingId) {
7155
+ return null;
7156
+ }
7157
+ const drawing = drawings.find((entry) => entry.id === selectedDrawingId);
7158
+ if (!drawing || !drawing.visible) {
7159
+ return null;
7160
+ }
7161
+ const bounds = getDrawingBounds(drawing);
7162
+ return bounds ? { drawing: serializeDrawing(drawing), bounds } : null;
7163
+ };
7164
+ const emitSelectionChangeIfMoved = () => {
7165
+ if (!selectionChangeHandler) {
7166
+ lastSelectionSignature = null;
7167
+ return;
7168
+ }
7169
+ const selection = getSelectedDrawingWithBounds();
7170
+ if (!selection || !drawState) {
7171
+ if (lastSelectionSignature !== null) {
7172
+ lastSelectionSignature = null;
7173
+ selectionChangeHandler(null);
7174
+ }
7175
+ return;
7176
+ }
7177
+ const dragging = drawingDragState !== null;
7178
+ const { bounds } = selection;
7179
+ const signature = `${selection.drawing.id}:${Math.round(bounds.left)}:${Math.round(bounds.top)}:${Math.round(
7180
+ bounds.right
7181
+ )}:${Math.round(bounds.bottom)}:${dragging ? 1 : 0}`;
7182
+ if (signature === lastSelectionSignature) {
7183
+ return;
7184
+ }
7185
+ lastSelectionSignature = signature;
7186
+ selectionChangeHandler({
7187
+ drawing: selection.drawing,
7188
+ bounds,
7189
+ plot: {
7190
+ left: drawState.chartLeft,
7191
+ top: drawState.chartTop,
7192
+ right: drawState.chartRight,
7193
+ bottom: drawState.chartBottom
7194
+ },
7195
+ dragging
7196
+ });
6914
7197
  };
6915
7198
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
6916
7199
  const dx = x2 - x1;
@@ -7878,6 +8161,10 @@ function createChart(element, options = {}) {
7878
8161
  if (event.pointerType === "touch" || event.pointerType === "pen") {
7879
8162
  event.preventDefault();
7880
8163
  }
8164
+ if (mergedOptions.accessibility?.focusable !== false && document.activeElement !== canvas) {
8165
+ focusFromPointer = true;
8166
+ canvas.focus({ preventScroll: true });
8167
+ }
7881
8168
  cancelKineticPan();
7882
8169
  panVelocitySamples.length = 0;
7883
8170
  magnetModifierActive = event.metaKey || event.ctrlKey;
@@ -7995,12 +8282,14 @@ function createChart(element, options = {}) {
7995
8282
  const drawingHit = getDrawingHit(point.x, point.y);
7996
8283
  if (drawingHit) {
7997
8284
  selectedDrawingId = drawingHit.drawing.id;
8285
+ const selectBounds = getDrawingBounds(drawingHit.drawing);
7998
8286
  drawingSelectHandler?.({
7999
8287
  drawing: serializeDrawing(drawingHit.drawing),
8000
8288
  target: drawingHit.target,
8001
8289
  ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex },
8002
8290
  x: point.x,
8003
- y: point.y
8291
+ y: point.y,
8292
+ ...selectBounds ? { bounds: selectBounds } : {}
8004
8293
  });
8005
8294
  if (!drawingHit.drawing.locked) {
8006
8295
  const startCanvasPoint = drawingPointFromCanvas(point.x, point.y, false);
@@ -8561,7 +8850,152 @@ function createChart(element, options = {}) {
8561
8850
  };
8562
8851
  const onContextMenu = (event) => {
8563
8852
  event.preventDefault();
8853
+ if (!contextMenuHandler || !drawState) {
8854
+ return;
8855
+ }
8856
+ const point = getCanvasPoint(event);
8857
+ const hitRegion = getHitRegion(point.x, point.y);
8858
+ if (hitRegion === "outside") {
8859
+ return;
8860
+ }
8861
+ const pane = hitRegion === "plot" ? getPaneAt(point.x, point.y) : null;
8862
+ const drawingHit = hitRegion === "plot" && !pane ? getDrawingHit(point.x, point.y) : null;
8863
+ if (drawingHit) {
8864
+ if (selectedDrawingId !== drawingHit.drawing.id) {
8865
+ selectedDrawingId = drawingHit.drawing.id;
8866
+ scheduleDraw();
8867
+ }
8868
+ }
8869
+ const region = drawingHit ? "drawing" : pane ? "indicator-pane" : hitRegion;
8870
+ const index = indexFromCanvasX(point.x);
8871
+ const barTime = index === null ? null : getTimeForIndex(index);
8872
+ const bar = index === null ? void 0 : data[index];
8873
+ const inMainPane = !pane && (hitRegion === "plot" || hitRegion === "y-axis");
8874
+ contextMenuHandler({
8875
+ region,
8876
+ x: point.x,
8877
+ y: point.y,
8878
+ clientX: event.clientX,
8879
+ clientY: event.clientY,
8880
+ ...inMainPane ? { price: roundToPricePrecision(priceFromCanvasY(point.y)) } : {},
8881
+ ...index === null ? {} : { index },
8882
+ ...barTime ? { time: barTime.toISOString() } : {},
8883
+ ...bar ? {
8884
+ point: {
8885
+ t: bar.time.toISOString(),
8886
+ o: bar.o,
8887
+ h: bar.h,
8888
+ l: bar.l,
8889
+ c: bar.c,
8890
+ ...bar.v === void 0 ? {} : { v: bar.v }
8891
+ }
8892
+ } : {},
8893
+ ...drawingHit ? {
8894
+ drawing: serializeDrawing(drawingHit.drawing),
8895
+ drawingTarget: drawingHit.target,
8896
+ ...drawingHit.pointIndex === void 0 ? {} : { pointIndex: drawingHit.pointIndex }
8897
+ } : {},
8898
+ ...pane ? { indicator: { id: pane.id, type: pane.type } } : {}
8899
+ });
8564
8900
  };
8901
+ const onCanvasKeyDown = (event) => {
8902
+ const keyboard = mergedOptions.keyboard ?? {};
8903
+ if (keyboard.enabled === false) {
8904
+ return;
8905
+ }
8906
+ if (event.altKey || event.ctrlKey || event.metaKey) {
8907
+ return;
8908
+ }
8909
+ const panStep = Math.max(1, keyboard.panBars ?? 1) * (event.shiftKey ? 10 : 1);
8910
+ const emit = (action, drawing) => {
8911
+ keyboardShortcutHandler?.({
8912
+ action,
8913
+ key: event.key,
8914
+ shiftKey: event.shiftKey,
8915
+ ...drawing ? { drawing } : {}
8916
+ });
8917
+ };
8918
+ switch (event.key) {
8919
+ case "Delete":
8920
+ case "Backspace": {
8921
+ if (keyboard.deleteSelectedDrawing === false || !selectedDrawingId) {
8922
+ return;
8923
+ }
8924
+ const target = drawings.find((entry) => entry.id === selectedDrawingId);
8925
+ if (!target) {
8926
+ return;
8927
+ }
8928
+ const removed = serializeDrawing(target);
8929
+ removeDrawing(selectedDrawingId);
8930
+ selectedDrawingId = null;
8931
+ scheduleDraw();
8932
+ emit("delete-drawing", removed);
8933
+ break;
8934
+ }
8935
+ case "Escape": {
8936
+ if (cancelDrawing()) {
8937
+ emit("cancel");
8938
+ break;
8939
+ }
8940
+ if (activeDrawingTool) {
8941
+ setActiveDrawingTool(null);
8942
+ emit("cancel");
8943
+ break;
8944
+ }
8945
+ if (selectedDrawingId) {
8946
+ setSelectedDrawing(null);
8947
+ emit("cancel");
8948
+ break;
8949
+ }
8950
+ return;
8951
+ }
8952
+ case "ArrowLeft":
8953
+ panX(-panStep);
8954
+ emit("pan-left");
8955
+ break;
8956
+ case "ArrowRight":
8957
+ panX(panStep);
8958
+ emit("pan-right");
8959
+ break;
8960
+ case "ArrowUp":
8961
+ case "ArrowDown": {
8962
+ if (!drawState) {
8963
+ return;
8964
+ }
8965
+ const step = (drawState.yMax - drawState.yMin) / 20 * (event.shiftKey ? 5 : 1);
8966
+ panY(event.key === "ArrowUp" ? step : -step);
8967
+ emit(event.key === "ArrowUp" ? "pan-up" : "pan-down");
8968
+ break;
8969
+ }
8970
+ case "+":
8971
+ case "=":
8972
+ zoomInX();
8973
+ emit("zoom-in");
8974
+ break;
8975
+ case "-":
8976
+ case "_":
8977
+ zoomOutX();
8978
+ emit("zoom-out");
8979
+ break;
8980
+ case "Home":
8981
+ fitContent();
8982
+ emit("reset-viewport");
8983
+ break;
8984
+ case "End":
8985
+ setFollowingLatest(true);
8986
+ emit("scroll-to-realtime");
8987
+ break;
8988
+ case "r":
8989
+ case "R":
8990
+ resetViewport();
8991
+ emit("reset-viewport");
8992
+ break;
8993
+ default:
8994
+ return;
8995
+ }
8996
+ event.preventDefault();
8997
+ };
8998
+ canvas.addEventListener("keydown", onCanvasKeyDown);
8565
8999
  canvas.addEventListener("pointerdown", onPointerDown);
8566
9000
  canvas.addEventListener("pointermove", onPointerMove);
8567
9001
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -8608,6 +9042,7 @@ function createChart(element, options = {}) {
8608
9042
  resetRightMarginCache();
8609
9043
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
8610
9044
  doubleClickAction = mergedOptions.doubleClickAction;
9045
+ applyAccessibilityAttributes();
8611
9046
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
8612
9047
  if (!isTickerSmoothingEnabled) {
8613
9048
  smoothedTickerPrice = null;
@@ -8924,6 +9359,16 @@ function createChart(element, options = {}) {
8924
9359
  magnetMode = mode;
8925
9360
  };
8926
9361
  const getMagnetMode = () => magnetMode;
9362
+ const setPriceScale = (options2) => {
9363
+ if (options2.mode === "linear" || options2.mode === "log" || options2.mode === "percent") {
9364
+ priceScaleMode = options2.mode;
9365
+ }
9366
+ if (typeof options2.inverted === "boolean") {
9367
+ priceScaleInverted = options2.inverted;
9368
+ }
9369
+ scheduleDraw();
9370
+ };
9371
+ const getPriceScale = () => ({ mode: priceScaleMode, inverted: priceScaleInverted });
8927
9372
  const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
8928
9373
  const setDrawings = (nextDrawings) => {
8929
9374
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
@@ -8981,6 +9426,23 @@ function createChart(element, options = {}) {
8981
9426
  const onDrawingHover = (handler) => {
8982
9427
  drawingHoverHandler = handler;
8983
9428
  };
9429
+ const onSelectionChange = (handler) => {
9430
+ selectionChangeHandler = handler;
9431
+ lastSelectionSignature = null;
9432
+ if (handler) {
9433
+ emitSelectionChangeIfMoved();
9434
+ }
9435
+ };
9436
+ const getSelectedDrawing = () => getSelectedDrawingWithBounds();
9437
+ const onContextMenuHandler = (handler) => {
9438
+ contextMenuHandler = handler;
9439
+ };
9440
+ const onKeyboardShortcut = (handler) => {
9441
+ keyboardShortcutHandler = handler;
9442
+ };
9443
+ const focus = () => {
9444
+ canvas.focus({ preventScroll: true });
9445
+ };
8984
9446
  const saveState = () => {
8985
9447
  const drawingDefaults = {};
8986
9448
  for (const [tool, defaults] of drawingToolDefaults) {
@@ -8993,6 +9455,7 @@ function createChart(element, options = {}) {
8993
9455
  drawings: getDrawings(),
8994
9456
  indicators: getIndicators(),
8995
9457
  magnetMode: getMagnetMode(),
9458
+ priceScale: getPriceScale(),
8996
9459
  drawingDefaults
8997
9460
  };
8998
9461
  };
@@ -9012,6 +9475,9 @@ function createChart(element, options = {}) {
9012
9475
  if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
9013
9476
  setMagnetMode(state.magnetMode);
9014
9477
  }
9478
+ if (state.priceScale && typeof state.priceScale === "object") {
9479
+ setPriceScale(state.priceScale);
9480
+ }
9015
9481
  if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
9016
9482
  for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
9017
9483
  if (defaults && typeof defaults === "object") {
@@ -9041,6 +9507,7 @@ function createChart(element, options = {}) {
9041
9507
  drawRafId = null;
9042
9508
  pendingDrawUpdateAutoScale = false;
9043
9509
  }
9510
+ canvas.removeEventListener("keydown", onCanvasKeyDown);
9044
9511
  canvas.removeEventListener("pointerdown", onPointerDown);
9045
9512
  canvas.removeEventListener("pointermove", onPointerMove);
9046
9513
  canvas.removeEventListener("pointerup", endPointerDrag);
@@ -9104,8 +9571,15 @@ function createChart(element, options = {}) {
9104
9571
  onDrawingSelect,
9105
9572
  onDrawingDoubleClick,
9106
9573
  onDrawingEditText,
9574
+ onSelectionChange,
9575
+ getSelectedDrawing,
9576
+ onContextMenu: onContextMenuHandler,
9577
+ onKeyboardShortcut,
9578
+ focus,
9107
9579
  setMagnetMode,
9108
9580
  getMagnetMode,
9581
+ setPriceScale,
9582
+ getPriceScale,
9109
9583
  cancelDrawing,
9110
9584
  setTradeMarkers,
9111
9585
  setSelectedDrawing,
@@ -9132,7 +9606,9 @@ function createChart(element, options = {}) {
9132
9606
  0 && (module.exports = {
9133
9607
  FIB_DEFAULT_PALETTE,
9134
9608
  POSITION_DEFAULT_COLORS,
9609
+ SCRIPT_SOURCE_INPUT_VALUES,
9135
9610
  compileScriptIndicator,
9136
9611
  createChart,
9612
+ extractScriptInputs,
9137
9613
  validateScriptSource
9138
9614
  });