hyperprop-charting-library 0.1.135 → 0.1.137

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.
package/dist/index.js CHANGED
@@ -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: [...] }" };
@@ -2949,6 +3035,24 @@ function createChart(element, options = {}) {
2949
3035
  let drawingDoubleClickHandler = null;
2950
3036
  let drawingEditTextHandler = null;
2951
3037
  let magnetMode = "none";
3038
+ let priceScaleMode = "linear";
3039
+ let priceScaleInverted = false;
3040
+ const PRICE_SCALE_LOG_FLOOR = 1e-9;
3041
+ const priceScaleTransform = (price) => priceScaleMode === "log" ? Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, price)) : price;
3042
+ const priceScaleUntransform = (value) => priceScaleMode === "log" ? Math.pow(10, value) : value;
3043
+ const percentBaselineForXStart = (xStartValue) => {
3044
+ if (priceScaleMode !== "percent") return null;
3045
+ const bar = data[clamp(Math.round(xStartValue), 0, Math.max(0, data.length - 1))];
3046
+ return bar && bar.c > 0 ? bar.c : null;
3047
+ };
3048
+ const formatPriceForScale = (price, percentBaseline) => {
3049
+ if (percentBaseline !== null) {
3050
+ const pct = (price / percentBaseline - 1) * 100;
3051
+ const rounded = Math.abs(pct) < 5e-3 ? 0 : pct;
3052
+ return `${rounded > 0 ? "+" : ""}${rounded.toFixed(2)}%`;
3053
+ }
3054
+ return formatPrice(price);
3055
+ };
2952
3056
  let magnetModifierActive = false;
2953
3057
  let shiftKeyActive = false;
2954
3058
  const MAGNET_WEAK_THRESHOLD_PX = 16;
@@ -4282,9 +4386,14 @@ function createChart(element, options = {}) {
4282
4386
  yMax
4283
4387
  };
4284
4388
  plotBottomForHit = fullChartBottom;
4389
+ const scaledYMin = priceScaleTransform(yMin);
4390
+ const scaledYRange = priceScaleTransform(yMax) - scaledYMin || 1;
4285
4391
  const yFromPrice = (price) => {
4286
- return chartBottom - (price - yMin) / yRange * chartHeight;
4392
+ const ratio = (priceScaleTransform(price) - scaledYMin) / scaledYRange;
4393
+ return priceScaleInverted ? chartTop + ratio * chartHeight : chartBottom - ratio * chartHeight;
4287
4394
  };
4395
+ const percentBaseline = percentBaselineForXStart(xStart);
4396
+ const formatAxisPrice = (price) => formatPriceForScale(price, percentBaseline);
4288
4397
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
4289
4398
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
4290
4399
  const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
@@ -4788,6 +4897,216 @@ function createChart(element, options = {}) {
4788
4897
  drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
4789
4898
  }
4790
4899
  }
4900
+ } else if (drawing.type === "ellipse") {
4901
+ const p0 = drawing.points[0];
4902
+ const p1 = drawing.points[1];
4903
+ if (p0 && p1) {
4904
+ const px0 = xFromDrawingPoint(p0);
4905
+ const px1 = xFromDrawingPoint(p1);
4906
+ const y0 = yFromPrice(p0.price);
4907
+ const y1 = yFromPrice(p1.price);
4908
+ const cx = (px0 + px1) / 2;
4909
+ const cy = (y0 + y1) / 2;
4910
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
4911
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
4912
+ ctx.save();
4913
+ ctx.globalAlpha = draft ? 0.6 : 1;
4914
+ ctx.beginPath();
4915
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
4916
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
4917
+ ctx.fill();
4918
+ ctx.lineWidth = Math.max(1, drawing.width);
4919
+ ctx.strokeStyle = drawing.color;
4920
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
4921
+ ctx.stroke();
4922
+ ctx.restore();
4923
+ handleAt(px0, y0, drawing.color);
4924
+ handleAt(px1, y1, drawing.color);
4925
+ handleAt(px0, y1, drawing.color);
4926
+ handleAt(px1, y0, drawing.color);
4927
+ if (drawing.label) {
4928
+ drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
4929
+ }
4930
+ }
4931
+ } else if (drawing.type === "parallel-channel") {
4932
+ const p0 = drawing.points[0];
4933
+ const p1 = drawing.points[1];
4934
+ const p2 = drawing.points[2];
4935
+ if (p0 && p1) {
4936
+ const x0 = xFromDrawingPoint(p0);
4937
+ const y0 = yFromPrice(p0.price);
4938
+ const x1 = xFromDrawingPoint(p1);
4939
+ const y1 = yFromPrice(p1.price);
4940
+ ctx.beginPath();
4941
+ ctx.moveTo(x0, y0);
4942
+ ctx.lineTo(x1, y1);
4943
+ ctx.stroke();
4944
+ handleAt(x0, y0, drawing.color);
4945
+ handleAt(x1, y1, drawing.color);
4946
+ if (p2) {
4947
+ const indexSpan = p1.index - p0.index;
4948
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
4949
+ const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
4950
+ const offset = p2.price - basePriceAtP2;
4951
+ const q0x = x0;
4952
+ const q0y = yFromPrice(p0.price + offset);
4953
+ const q1x = x1;
4954
+ const q1y = yFromPrice(p1.price + offset);
4955
+ ctx.save();
4956
+ ctx.globalAlpha = draft ? 0.5 : 1;
4957
+ ctx.fillStyle = hexToRgba(drawing.color, 0.08);
4958
+ ctx.beginPath();
4959
+ ctx.moveTo(x0, y0);
4960
+ ctx.lineTo(x1, y1);
4961
+ ctx.lineTo(q1x, q1y);
4962
+ ctx.lineTo(q0x, q0y);
4963
+ ctx.closePath();
4964
+ ctx.fill();
4965
+ ctx.restore();
4966
+ ctx.beginPath();
4967
+ ctx.moveTo(q0x, q0y);
4968
+ ctx.lineTo(q1x, q1y);
4969
+ ctx.stroke();
4970
+ ctx.save();
4971
+ ctx.setLineDash([4, 4]);
4972
+ ctx.globalAlpha = draft ? 0.5 : 0.7;
4973
+ ctx.beginPath();
4974
+ ctx.moveTo(x0, (y0 + q0y) / 2);
4975
+ ctx.lineTo(x1, (y1 + q1y) / 2);
4976
+ ctx.stroke();
4977
+ ctx.restore();
4978
+ handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
4979
+ }
4980
+ if (drawing.label) {
4981
+ drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
4982
+ }
4983
+ }
4984
+ } else if (drawing.type === "arrow") {
4985
+ const first = drawing.points[0];
4986
+ const second = drawing.points[1];
4987
+ if (first && second) {
4988
+ const x0 = xFromDrawingPoint(first);
4989
+ const y0 = yFromPrice(first.price);
4990
+ const x1 = xFromDrawingPoint(second);
4991
+ const y1 = yFromPrice(second.price);
4992
+ const angle = Math.atan2(y1 - y0, x1 - x0);
4993
+ const head = 5 + Math.max(1, drawing.width) * 3;
4994
+ const shaftX = x1 - Math.cos(angle) * head * 0.6;
4995
+ const shaftY = y1 - Math.sin(angle) * head * 0.6;
4996
+ ctx.beginPath();
4997
+ ctx.moveTo(x0, y0);
4998
+ ctx.lineTo(shaftX, shaftY);
4999
+ ctx.stroke();
5000
+ ctx.save();
5001
+ ctx.setLineDash([]);
5002
+ ctx.fillStyle = drawing.color;
5003
+ ctx.beginPath();
5004
+ ctx.moveTo(x1, y1);
5005
+ ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
5006
+ ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
5007
+ ctx.closePath();
5008
+ ctx.fill();
5009
+ ctx.restore();
5010
+ handleAt(x0, y0, drawing.color);
5011
+ handleAt(x1, y1, drawing.color);
5012
+ if (drawing.label) {
5013
+ const midX = (x0 + x1) / 2;
5014
+ const midY = (y0 + y1) / 2;
5015
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
5016
+ }
5017
+ }
5018
+ } else if (drawing.type === "brush") {
5019
+ const pts = drawing.points;
5020
+ if (pts.length >= 2) {
5021
+ ctx.save();
5022
+ ctx.lineJoin = "round";
5023
+ ctx.lineCap = "round";
5024
+ ctx.beginPath();
5025
+ const startX = xFromDrawingPoint(pts[0]);
5026
+ const startY = yFromPrice(pts[0].price);
5027
+ ctx.moveTo(startX, startY);
5028
+ if (pts.length === 2) {
5029
+ ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
5030
+ } else {
5031
+ for (let i = 1; i < pts.length - 1; i += 1) {
5032
+ const cxp = xFromDrawingPoint(pts[i]);
5033
+ const cyp = yFromPrice(pts[i].price);
5034
+ const nx = xFromDrawingPoint(pts[i + 1]);
5035
+ const ny = yFromPrice(pts[i + 1].price);
5036
+ ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
5037
+ }
5038
+ const last = pts[pts.length - 1];
5039
+ ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
5040
+ }
5041
+ ctx.stroke();
5042
+ ctx.restore();
5043
+ if (isSelected) {
5044
+ handleAt(startX, startY, drawing.color);
5045
+ const last = pts[pts.length - 1];
5046
+ handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
5047
+ }
5048
+ }
5049
+ } else if (drawing.type === "callout") {
5050
+ const anchor = drawing.points[0];
5051
+ const boxPoint = drawing.points[1];
5052
+ if (anchor && boxPoint) {
5053
+ const ax = xFromDrawingPoint(anchor);
5054
+ const ay = yFromPrice(anchor.price);
5055
+ const bx = xFromDrawingPoint(boxPoint);
5056
+ const by = yFromPrice(boxPoint.price);
5057
+ const fontSize = Math.max(6, drawing.fontSize);
5058
+ const prevFont = ctx.font;
5059
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
5060
+ const rawLabel = drawing.label ?? "";
5061
+ const isPlaceholder = rawLabel.trim().length === 0;
5062
+ const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
5063
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
5064
+ const padX = 10;
5065
+ const padY = 7;
5066
+ const lineH = Math.round(fontSize * 1.35);
5067
+ const blockW = textW + padX * 2;
5068
+ const blockH = lines.length * lineH + padY * 2;
5069
+ const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
5070
+ const edgeY = Math.max(by, Math.min(by + blockH, ay));
5071
+ const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
5072
+ if (!insideBox) {
5073
+ ctx.save();
5074
+ ctx.setLineDash([]);
5075
+ ctx.lineWidth = Math.max(1, drawing.width);
5076
+ ctx.beginPath();
5077
+ ctx.moveTo(edgeX, edgeY);
5078
+ ctx.lineTo(ax, ay);
5079
+ ctx.stroke();
5080
+ const angle = Math.atan2(ay - edgeY, ax - edgeX);
5081
+ const head = 7;
5082
+ ctx.fillStyle = drawing.color;
5083
+ ctx.beginPath();
5084
+ ctx.moveTo(ax, ay);
5085
+ ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
5086
+ ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
5087
+ ctx.closePath();
5088
+ ctx.fill();
5089
+ ctx.restore();
5090
+ }
5091
+ ctx.save();
5092
+ ctx.setLineDash([]);
5093
+ ctx.fillStyle = hexToRgba(drawing.color, 0.16);
5094
+ fillRoundedRect(bx, by, blockW, blockH, 6);
5095
+ ctx.strokeStyle = drawing.color;
5096
+ ctx.lineWidth = Math.max(1, drawing.width);
5097
+ strokeRoundedRect(bx, by, blockW, blockH, 6);
5098
+ ctx.fillStyle = drawing.color;
5099
+ ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
5100
+ ctx.textAlign = "left";
5101
+ ctx.textBaseline = "middle";
5102
+ lines.forEach((line, lineIndex) => {
5103
+ ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
5104
+ });
5105
+ ctx.restore();
5106
+ ctx.font = prevFont;
5107
+ handleAt(ax, ay, drawing.color);
5108
+ handleAt(bx, by, drawing.color);
5109
+ }
4791
5110
  } else if (drawing.type === "price-range") {
4792
5111
  const p0 = drawing.points[0];
4793
5112
  const p1 = drawing.points[1];
@@ -4981,19 +5300,40 @@ function createChart(element, options = {}) {
4981
5300
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
4982
5301
  const gridColor = grid.color ?? mergedOptions.gridColor;
4983
5302
  const priceTicks = [];
4984
- {
4985
- const targetStep = yRange / Math.max(1, yTicks);
4986
- let step;
4987
- if (targetStep > 0 && Number.isFinite(targetStep)) {
4988
- const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
4989
- const frac = targetStep / base;
4990
- step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
4991
- const instrumentTick = getConfiguredTickSize();
4992
- if (instrumentTick > 0 && step < instrumentTick * 1e6) {
4993
- step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5303
+ const pickNiceStep = (targetStep) => {
5304
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return 1;
5305
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5306
+ const frac = targetStep / base;
5307
+ return (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5308
+ };
5309
+ if (percentBaseline !== null) {
5310
+ const pctMin = (yMin / percentBaseline - 1) * 100;
5311
+ const pctMax = (yMax / percentBaseline - 1) * 100;
5312
+ const step = pickNiceStep((pctMax - pctMin) / Math.max(1, yTicks));
5313
+ const firstTick = Math.ceil(pctMin / step) * step;
5314
+ for (let i = 0; ; i += 1) {
5315
+ const pct = firstTick + i * step;
5316
+ if (pct > pctMax + step * 1e-6) break;
5317
+ priceTicks.push(percentBaseline * (1 + pct / 100));
5318
+ if (priceTicks.length > 200) break;
5319
+ }
5320
+ } else if (priceScaleMode === "log" && yMin > 0 && yMax / yMin > 4) {
5321
+ const mantissas = Math.log10(yMax / yMin) > 8 ? [1] : [1, 2, 5];
5322
+ const firstDecade = Math.floor(Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, yMin)));
5323
+ const lastDecade = Math.ceil(Math.log10(yMax));
5324
+ for (let decade = firstDecade; decade <= lastDecade; decade += 1) {
5325
+ for (const mantissa of mantissas) {
5326
+ const price = mantissa * Math.pow(10, decade);
5327
+ if (price >= yMin && price <= yMax) priceTicks.push(price);
5328
+ if (priceTicks.length > 200) break;
4994
5329
  }
4995
- } else {
4996
- step = 1;
5330
+ }
5331
+ } else {
5332
+ const targetStep = yRange / Math.max(1, yTicks);
5333
+ let step = pickNiceStep(targetStep);
5334
+ const instrumentTick = getConfiguredTickSize();
5335
+ if (targetStep > 0 && Number.isFinite(targetStep) && instrumentTick > 0 && step < instrumentTick * 1e6) {
5336
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
4997
5337
  }
4998
5338
  const firstTick = Math.ceil(yMin / step) * step;
4999
5339
  for (let i = 0; ; i += 1) {
@@ -5756,7 +6096,7 @@ function createChart(element, options = {}) {
5756
6096
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
5757
6097
  for (const price of priceTicks) {
5758
6098
  const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
5759
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6099
+ drawText(formatAxisPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
5760
6100
  }
5761
6101
  ctx.font = prevFont;
5762
6102
  }
@@ -5831,7 +6171,7 @@ function createChart(element, options = {}) {
5831
6171
  ...ticker.showCountdownInLabel ? [getCountdownText()] : []
5832
6172
  ].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
5833
6173
  addPriceAxisLabel({
5834
- text: formatPrice(tickerPrice),
6174
+ text: formatAxisPrice(tickerPrice),
5835
6175
  ...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
5836
6176
  subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
5837
6177
  ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
@@ -5858,7 +6198,7 @@ function createChart(element, options = {}) {
5858
6198
  const previousClose = previousCloseCandidate;
5859
6199
  drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
5860
6200
  addPriceAxisLabel({
5861
- text: `PDC ${formatPrice(previousClose)}`,
6201
+ text: `PDC ${formatAxisPrice(previousClose)}`,
5862
6202
  price: previousClose,
5863
6203
  backgroundColor: labels.backgroundColor,
5864
6204
  textColor: labels.mutedTextColor,
@@ -5877,7 +6217,7 @@ function createChart(element, options = {}) {
5877
6217
  if (point.l < visibleLow) visibleLow = point.l;
5878
6218
  }
5879
6219
  addPriceAxisLabel({
5880
- text: `H ${formatPrice(visibleHigh)}`,
6220
+ text: `H ${formatAxisPrice(visibleHigh)}`,
5881
6221
  price: visibleHigh,
5882
6222
  backgroundColor: labels.backgroundColor,
5883
6223
  textColor: labels.textColor,
@@ -5885,7 +6225,7 @@ function createChart(element, options = {}) {
5885
6225
  priority: 40
5886
6226
  });
5887
6227
  addPriceAxisLabel({
5888
- text: `L ${formatPrice(visibleLow)}`,
6228
+ text: `L ${formatAxisPrice(visibleLow)}`,
5889
6229
  price: visibleLow,
5890
6230
  backgroundColor: labels.backgroundColor,
5891
6231
  textColor: labels.textColor,
@@ -5897,7 +6237,7 @@ function createChart(element, options = {}) {
5897
6237
  if (Number.isFinite(labels.bidPrice)) {
5898
6238
  drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
5899
6239
  addPriceAxisLabel({
5900
- text: `B ${formatPrice(labels.bidPrice)}`,
6240
+ text: `B ${formatAxisPrice(labels.bidPrice)}`,
5901
6241
  price: labels.bidPrice,
5902
6242
  backgroundColor: labels.bidColor,
5903
6243
  textColor: "#0b1220",
@@ -5908,7 +6248,7 @@ function createChart(element, options = {}) {
5908
6248
  if (Number.isFinite(labels.askPrice)) {
5909
6249
  drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
5910
6250
  addPriceAxisLabel({
5911
- text: `A ${formatPrice(labels.askPrice)}`,
6251
+ text: `A ${formatAxisPrice(labels.askPrice)}`,
5912
6252
  price: labels.askPrice,
5913
6253
  backgroundColor: labels.askColor,
5914
6254
  textColor: "#0b1220",
@@ -6169,8 +6509,11 @@ function createChart(element, options = {}) {
6169
6509
  ctx.restore();
6170
6510
  };
6171
6511
  if (crosshair.showPriceLabel) {
6172
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
6173
- const priceText = formatPrice(hoverPrice);
6512
+ const scaledYMin = priceScaleTransform(yMin);
6513
+ const scaledYRange = priceScaleTransform(yMin + yRange) - scaledYMin || 1;
6514
+ const hoverRatio = priceScaleInverted ? (cy - chartTop) / chartHeight : (chartBottom - cy) / chartHeight;
6515
+ const hoverPrice = priceScaleUntransform(scaledYMin + hoverRatio * scaledYRange);
6516
+ const priceText = formatPriceForScale(hoverPrice, percentBaselineForXStart(xStart));
6174
6517
  const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
6175
6518
  const priceX = getRightAxisLabelX(chartRight);
6176
6519
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
@@ -6338,8 +6681,8 @@ function createChart(element, options = {}) {
6338
6681
  const maxRange = bounds.hardSpan;
6339
6682
  const nextRange = clamp(currentRange * factor, minRange, maxRange);
6340
6683
  const anchorRatio = clamp((anchorY - drawState.chartTop) / drawState.chartHeight, 0, 1);
6341
- const anchorPrice = currentMax - anchorRatio * currentRange;
6342
- const nextMax = anchorPrice + anchorRatio * nextRange;
6684
+ const anchorPrice = priceScaleInverted ? currentMin + anchorRatio * currentRange : currentMax - anchorRatio * currentRange;
6685
+ const nextMax = priceScaleInverted ? anchorPrice + (1 - anchorRatio) * nextRange : anchorPrice + anchorRatio * nextRange;
6343
6686
  const nextMin = nextMax - nextRange;
6344
6687
  const clamped = clampYRange(nextMin, nextMax);
6345
6688
  yMinOverride = clamped.min;
@@ -6363,7 +6706,7 @@ function createChart(element, options = {}) {
6363
6706
  const currentMin = yMinOverride ?? drawState.yMin;
6364
6707
  const currentMax = yMaxOverride ?? drawState.yMax;
6365
6708
  const range = currentMax - currentMin || 1;
6366
- const priceShift = deltaY / drawState.chartHeight * range;
6709
+ const priceShift = deltaY / drawState.chartHeight * range * (priceScaleInverted ? -1 : 1);
6367
6710
  const clamped = clampYRange(currentMin + priceShift, currentMax + priceShift);
6368
6711
  yMinOverride = clamped.min;
6369
6712
  yMaxOverride = clamped.max;
@@ -6596,8 +6939,14 @@ function createChart(element, options = {}) {
6596
6939
  if (!drawState) {
6597
6940
  return 0;
6598
6941
  }
6599
- const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
6600
- return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
6942
+ const ratio = clamp(
6943
+ priceScaleInverted ? (y - drawState.chartTop) / drawState.chartHeight : (drawState.chartBottom - y) / drawState.chartHeight,
6944
+ 0,
6945
+ 1
6946
+ );
6947
+ const scaledMin = priceScaleTransform(drawState.yMin);
6948
+ const scaledMax = priceScaleTransform(drawState.yMax);
6949
+ return priceScaleUntransform(scaledMin + ratio * (scaledMax - scaledMin));
6601
6950
  };
6602
6951
  const applyMagnet = (y, index, price) => {
6603
6952
  const effective = magnetModifierActive ? "strong" : magnetMode;
@@ -6669,8 +7018,10 @@ function createChart(element, options = {}) {
6669
7018
  };
6670
7019
  const canvasYFromDrawingPrice = (price) => {
6671
7020
  if (!drawState) return 0;
6672
- const range = drawState.yMax - drawState.yMin || 1;
6673
- return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
7021
+ const scaledMin = priceScaleTransform(drawState.yMin);
7022
+ const scaledRange = priceScaleTransform(drawState.yMax) - scaledMin || 1;
7023
+ const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7024
+ return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
6674
7025
  };
6675
7026
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
6676
7027
  const dx = x2 - x1;
@@ -6842,6 +7193,126 @@ function createChart(element, options = {}) {
6842
7193
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
6843
7194
  return { drawing, target: "line" };
6844
7195
  }
7196
+ } else if (drawing.type === "ellipse") {
7197
+ const p0 = drawing.points[0];
7198
+ const p1 = drawing.points[1];
7199
+ if (!p0 || !p1) continue;
7200
+ const px0 = canvasXFromDrawingPoint(p0);
7201
+ const px1 = canvasXFromDrawingPoint(p1);
7202
+ const y0 = canvasYFromDrawingPrice(p0.price);
7203
+ const y1 = canvasYFromDrawingPrice(p1.price);
7204
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7205
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7206
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7207
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7208
+ const cx = (px0 + px1) / 2;
7209
+ const cy = (y0 + y1) / 2;
7210
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7211
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7212
+ const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7213
+ if (norm <= 1) {
7214
+ return { drawing, target: "line" };
7215
+ }
7216
+ } else if (drawing.type === "parallel-channel") {
7217
+ const p0 = drawing.points[0];
7218
+ const p1 = drawing.points[1];
7219
+ const p2 = drawing.points[2];
7220
+ if (!p0 || !p1) continue;
7221
+ const x0 = canvasXFromDrawingPoint(p0);
7222
+ const y0 = canvasYFromDrawingPrice(p0.price);
7223
+ const x1 = canvasXFromDrawingPoint(p1);
7224
+ const y1 = canvasYFromDrawingPrice(p1.price);
7225
+ if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7226
+ if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7227
+ if (p2) {
7228
+ const x2 = canvasXFromDrawingPoint(p2);
7229
+ const y2 = canvasYFromDrawingPrice(p2.price);
7230
+ if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7231
+ const indexSpan = p1.index - p0.index;
7232
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7233
+ const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7234
+ const q0y = canvasYFromDrawingPrice(p0.price + offset);
7235
+ const q1y = canvasYFromDrawingPrice(p1.price + offset);
7236
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7237
+ return { drawing, target: "line" };
7238
+ }
7239
+ const minX = Math.min(x0, x1);
7240
+ const maxX = Math.max(x0, x1);
7241
+ if (x >= minX && x <= maxX && maxX > minX) {
7242
+ const t = (x - x0) / (x1 - x0);
7243
+ const baseY = y0 + t * (y1 - y0);
7244
+ const parY = q0y + t * (q1y - q0y);
7245
+ if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
7246
+ return { drawing, target: "line" };
7247
+ }
7248
+ }
7249
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7250
+ return { drawing, target: "line" };
7251
+ }
7252
+ } else if (drawing.type === "arrow") {
7253
+ const first = drawing.points[0];
7254
+ const second = drawing.points[1];
7255
+ if (!first || !second) continue;
7256
+ const x1 = canvasXFromDrawingPoint(first);
7257
+ const y1 = canvasYFromDrawingPrice(first.price);
7258
+ const x2 = canvasXFromDrawingPoint(second);
7259
+ const y2 = canvasYFromDrawingPrice(second.price);
7260
+ if (Math.hypot(x - x1, y - y1) <= 8) {
7261
+ return { drawing, target: "handle", pointIndex: 0 };
7262
+ }
7263
+ if (Math.hypot(x - x2, y - y2) <= 8) {
7264
+ return { drawing, target: "handle", pointIndex: 1 };
7265
+ }
7266
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7267
+ return { drawing, target: "line" };
7268
+ }
7269
+ } else if (drawing.type === "brush") {
7270
+ const pts = drawing.points;
7271
+ if (pts.length < 2) continue;
7272
+ let hitBody = false;
7273
+ let prevX = canvasXFromDrawingPoint(pts[0]);
7274
+ let prevY = canvasYFromDrawingPrice(pts[0].price);
7275
+ for (let i = 1; i < pts.length; i += 1) {
7276
+ const nextX = canvasXFromDrawingPoint(pts[i]);
7277
+ const nextY = canvasYFromDrawingPrice(pts[i].price);
7278
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7279
+ hitBody = true;
7280
+ break;
7281
+ }
7282
+ prevX = nextX;
7283
+ prevY = nextY;
7284
+ }
7285
+ if (hitBody) {
7286
+ return { drawing, target: "line" };
7287
+ }
7288
+ } else if (drawing.type === "callout") {
7289
+ const anchor = drawing.points[0];
7290
+ const boxPoint = drawing.points[1];
7291
+ if (!anchor || !boxPoint) continue;
7292
+ const ax = canvasXFromDrawingPoint(anchor);
7293
+ const ay = canvasYFromDrawingPrice(anchor.price);
7294
+ const bx = canvasXFromDrawingPoint(boxPoint);
7295
+ const by = canvasYFromDrawingPrice(boxPoint.price);
7296
+ if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7297
+ if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7298
+ const fontSize = Math.max(6, drawing.fontSize);
7299
+ const prevFont = ctx.font;
7300
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
7301
+ const rawLabel = drawing.label ?? "";
7302
+ const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
7303
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
7304
+ ctx.font = prevFont;
7305
+ const padX = 10;
7306
+ const padY = 7;
7307
+ const lineH = Math.round(fontSize * 1.35);
7308
+ const blockW = textW + padX * 2;
7309
+ const blockH = lines.length * lineH + padY * 2;
7310
+ if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7311
+ return { drawing, target: "line" };
7312
+ }
7313
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7314
+ return { drawing, target: "line" };
7315
+ }
6845
7316
  } else if (drawing.type === "price-range") {
6846
7317
  const p0 = drawing.points[0];
6847
7318
  const p1 = drawing.points[1];
@@ -7070,6 +7541,128 @@ function createChart(element, options = {}) {
7070
7541
  scheduleDraw();
7071
7542
  return true;
7072
7543
  }
7544
+ if (activeDrawingTool === "arrow") {
7545
+ if (draftDrawing?.type === "arrow") {
7546
+ const anchor = draftDrawing.points[0];
7547
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
7548
+ const completed = normalizeDrawingState({
7549
+ ...serializeDrawing(draftDrawing),
7550
+ points: [anchor, endPoint]
7551
+ });
7552
+ drawings.push(completed);
7553
+ draftDrawing = null;
7554
+ activeDrawingTool = null;
7555
+ emitDrawingsChange();
7556
+ scheduleDraw();
7557
+ return true;
7558
+ }
7559
+ const defaults = getDrawingToolDefaults("arrow");
7560
+ draftDrawing = normalizeDrawingState({
7561
+ type: "arrow",
7562
+ points: [point, point],
7563
+ color: defaults.color ?? "#2563eb",
7564
+ style: defaults.style ?? "solid",
7565
+ width: defaults.width ?? 2
7566
+ });
7567
+ scheduleDraw();
7568
+ return true;
7569
+ }
7570
+ if (activeDrawingTool === "parallel-channel") {
7571
+ if (draftDrawing?.type === "parallel-channel") {
7572
+ if (draftDrawing.points.length < 3) {
7573
+ draftDrawing = normalizeDrawingState({
7574
+ ...serializeDrawing(draftDrawing),
7575
+ points: [...draftDrawing.points.slice(0, -1), point, point]
7576
+ });
7577
+ scheduleDraw();
7578
+ return true;
7579
+ }
7580
+ const completed = normalizeDrawingState({
7581
+ ...serializeDrawing(draftDrawing),
7582
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
7583
+ });
7584
+ drawings.push(completed);
7585
+ draftDrawing = null;
7586
+ activeDrawingTool = null;
7587
+ emitDrawingsChange();
7588
+ scheduleDraw();
7589
+ return true;
7590
+ }
7591
+ const defaults = getDrawingToolDefaults("parallel-channel");
7592
+ draftDrawing = normalizeDrawingState({
7593
+ type: "parallel-channel",
7594
+ points: [point, point],
7595
+ color: defaults.color ?? "#2563eb",
7596
+ style: defaults.style ?? "solid",
7597
+ width: defaults.width ?? 2
7598
+ });
7599
+ scheduleDraw();
7600
+ return true;
7601
+ }
7602
+ if (activeDrawingTool === "ellipse") {
7603
+ if (draftDrawing?.type === "ellipse") {
7604
+ const completed = normalizeDrawingState({
7605
+ ...serializeDrawing(draftDrawing),
7606
+ points: [draftDrawing.points[0], point]
7607
+ });
7608
+ drawings.push(completed);
7609
+ draftDrawing = null;
7610
+ activeDrawingTool = null;
7611
+ emitDrawingsChange();
7612
+ scheduleDraw();
7613
+ return true;
7614
+ }
7615
+ const defaults = getDrawingToolDefaults("ellipse");
7616
+ draftDrawing = normalizeDrawingState({
7617
+ type: "ellipse",
7618
+ points: [point, point],
7619
+ color: defaults.color ?? "#2962ff",
7620
+ style: defaults.style ?? "solid",
7621
+ width: defaults.width ?? 1
7622
+ });
7623
+ scheduleDraw();
7624
+ return true;
7625
+ }
7626
+ if (activeDrawingTool === "callout") {
7627
+ if (draftDrawing?.type === "callout") {
7628
+ const completed = normalizeDrawingState({
7629
+ ...serializeDrawing(draftDrawing),
7630
+ points: [draftDrawing.points[0], point]
7631
+ });
7632
+ drawings.push(completed);
7633
+ draftDrawing = null;
7634
+ activeDrawingTool = null;
7635
+ selectedDrawingId = completed.id;
7636
+ emitDrawingsChange();
7637
+ scheduleDraw();
7638
+ drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
7639
+ return true;
7640
+ }
7641
+ const defaults = getDrawingToolDefaults("callout");
7642
+ draftDrawing = normalizeDrawingState({
7643
+ type: "callout",
7644
+ points: [point, point],
7645
+ color: defaults.color ?? "#2962ff",
7646
+ style: defaults.style ?? "solid",
7647
+ width: defaults.width ?? 1,
7648
+ ...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
7649
+ label: ""
7650
+ });
7651
+ scheduleDraw();
7652
+ return true;
7653
+ }
7654
+ if (activeDrawingTool === "brush") {
7655
+ const defaults = getDrawingToolDefaults("brush");
7656
+ draftDrawing = normalizeDrawingState({
7657
+ type: "brush",
7658
+ points: [point],
7659
+ color: defaults.color ?? "#f59e0b",
7660
+ style: defaults.style ?? "solid",
7661
+ width: defaults.width ?? 2
7662
+ });
7663
+ scheduleDraw();
7664
+ return true;
7665
+ }
7073
7666
  if (activeDrawingTool === "fib-retracement") {
7074
7667
  if (draftDrawing?.type === "fib-retracement") {
7075
7668
  const completed = normalizeDrawingState({
@@ -7273,7 +7866,7 @@ function createChart(element, options = {}) {
7273
7866
  }
7274
7867
  return { ...drawing, points: pts };
7275
7868
  }
7276
- if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
7869
+ if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
7277
7870
  const pts = drawing.points.map((point) => ({ ...point }));
7278
7871
  const p0 = pts[0];
7279
7872
  const p1 = pts[1];
@@ -7292,7 +7885,7 @@ function createChart(element, options = {}) {
7292
7885
  }
7293
7886
  return { ...drawing, points: pts };
7294
7887
  }
7295
- if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
7888
+ if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
7296
7889
  const pointIndex = drawingDragState.pointIndex ?? 0;
7297
7890
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
7298
7891
  const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
@@ -7542,6 +8135,10 @@ function createChart(element, options = {}) {
7542
8135
  if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
7543
8136
  setCrosshairPoint(null);
7544
8137
  canvas.style.cursor = "crosshair";
8138
+ if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
8139
+ activePointerId = event.pointerId;
8140
+ capturePointer(event.pointerId);
8141
+ }
7545
8142
  return;
7546
8143
  }
7547
8144
  isDragging = true;
@@ -7626,9 +8223,30 @@ function createChart(element, options = {}) {
7626
8223
  setCrosshairPoint(null);
7627
8224
  return;
7628
8225
  }
7629
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
8226
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8227
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
8228
+ return;
8229
+ }
8230
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
8231
+ if (nextPoint) {
8232
+ const last = draftDrawing.points[draftDrawing.points.length - 1];
8233
+ const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
8234
+ const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
8235
+ if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
8236
+ draftDrawing = {
8237
+ ...draftDrawing,
8238
+ points: [...draftDrawing.points, nextPoint]
8239
+ };
8240
+ }
8241
+ canvas.style.cursor = "crosshair";
8242
+ setCrosshairPoint(null);
8243
+ scheduleDraw();
8244
+ }
8245
+ return;
8246
+ }
8247
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
7630
8248
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
7631
- if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
8249
+ if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
7632
8250
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
7633
8251
  }
7634
8252
  if (nextPoint) {
@@ -7865,6 +8483,25 @@ function createChart(element, options = {}) {
7865
8483
  scheduleDraw();
7866
8484
  return;
7867
8485
  }
8486
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8487
+ if (draftDrawing.points.length >= 2) {
8488
+ const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
8489
+ drawings.push(completed);
8490
+ draftDrawing = null;
8491
+ activeDrawingTool = null;
8492
+ canvas.style.cursor = "default";
8493
+ emitDrawingsChange();
8494
+ } else {
8495
+ draftDrawing = null;
8496
+ canvas.style.cursor = "crosshair";
8497
+ }
8498
+ isDragging = false;
8499
+ dragMode = null;
8500
+ activePointerId = null;
8501
+ pointerDownInfo = null;
8502
+ scheduleDraw();
8503
+ return;
8504
+ }
7868
8505
  if (orderDragState) {
7869
8506
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7870
8507
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -8008,7 +8645,7 @@ function createChart(element, options = {}) {
8008
8645
  x: point.x,
8009
8646
  y: point.y
8010
8647
  };
8011
- if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
8648
+ if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
8012
8649
  drawingEditTextHandler?.(payload);
8013
8650
  } else {
8014
8651
  drawingDoubleClickHandler?.(payload);
@@ -8398,6 +9035,16 @@ function createChart(element, options = {}) {
8398
9035
  magnetMode = mode;
8399
9036
  };
8400
9037
  const getMagnetMode = () => magnetMode;
9038
+ const setPriceScale = (options2) => {
9039
+ if (options2.mode === "linear" || options2.mode === "log" || options2.mode === "percent") {
9040
+ priceScaleMode = options2.mode;
9041
+ }
9042
+ if (typeof options2.inverted === "boolean") {
9043
+ priceScaleInverted = options2.inverted;
9044
+ }
9045
+ scheduleDraw();
9046
+ };
9047
+ const getPriceScale = () => ({ mode: priceScaleMode, inverted: priceScaleInverted });
8401
9048
  const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
8402
9049
  const setDrawings = (nextDrawings) => {
8403
9050
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
@@ -8455,6 +9102,55 @@ function createChart(element, options = {}) {
8455
9102
  const onDrawingHover = (handler) => {
8456
9103
  drawingHoverHandler = handler;
8457
9104
  };
9105
+ const saveState = () => {
9106
+ const drawingDefaults = {};
9107
+ for (const [tool, defaults] of drawingToolDefaults) {
9108
+ drawingDefaults[tool] = { ...defaults };
9109
+ }
9110
+ return {
9111
+ version: 1,
9112
+ chartType: getChartType(),
9113
+ viewport: getViewport(),
9114
+ drawings: getDrawings(),
9115
+ indicators: getIndicators(),
9116
+ magnetMode: getMagnetMode(),
9117
+ priceScale: getPriceScale(),
9118
+ drawingDefaults
9119
+ };
9120
+ };
9121
+ const loadState = (state) => {
9122
+ if (!state || typeof state !== "object") {
9123
+ return;
9124
+ }
9125
+ if (typeof state.chartType === "string") {
9126
+ setChartType(state.chartType);
9127
+ }
9128
+ if (Array.isArray(state.indicators)) {
9129
+ setIndicators(state.indicators);
9130
+ }
9131
+ if (Array.isArray(state.drawings)) {
9132
+ setDrawings(state.drawings);
9133
+ }
9134
+ if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
9135
+ setMagnetMode(state.magnetMode);
9136
+ }
9137
+ if (state.priceScale && typeof state.priceScale === "object") {
9138
+ setPriceScale(state.priceScale);
9139
+ }
9140
+ if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
9141
+ for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
9142
+ if (defaults && typeof defaults === "object") {
9143
+ setDrawingDefaults(tool, defaults);
9144
+ }
9145
+ }
9146
+ }
9147
+ if (state.viewport && typeof state.viewport === "object") {
9148
+ setViewport(state.viewport);
9149
+ }
9150
+ };
9151
+ const takeScreenshot = (options2 = {}) => {
9152
+ return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
9153
+ };
8458
9154
  const destroy = () => {
8459
9155
  cancelKineticPan();
8460
9156
  if (smoothingRafId !== null) {
@@ -8535,6 +9231,8 @@ function createChart(element, options = {}) {
8535
9231
  onDrawingEditText,
8536
9232
  setMagnetMode,
8537
9233
  getMagnetMode,
9234
+ setPriceScale,
9235
+ getPriceScale,
8538
9236
  cancelDrawing,
8539
9237
  setTradeMarkers,
8540
9238
  setSelectedDrawing,
@@ -8550,6 +9248,9 @@ function createChart(element, options = {}) {
8550
9248
  updateIndicator,
8551
9249
  removeIndicator,
8552
9250
  setIndicators,
9251
+ saveState,
9252
+ loadState,
9253
+ takeScreenshot,
8553
9254
  resize,
8554
9255
  destroy
8555
9256
  };
@@ -8557,7 +9258,9 @@ function createChart(element, options = {}) {
8557
9258
  export {
8558
9259
  FIB_DEFAULT_PALETTE,
8559
9260
  POSITION_DEFAULT_COLORS,
9261
+ SCRIPT_SOURCE_INPUT_VALUES,
8560
9262
  compileScriptIndicator,
8561
9263
  createChart,
9264
+ extractScriptInputs,
8562
9265
  validateScriptSource
8563
9266
  };