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.cjs CHANGED
@@ -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: [...] }" };
@@ -2979,6 +3067,24 @@ function createChart(element, options = {}) {
2979
3067
  let drawingDoubleClickHandler = null;
2980
3068
  let drawingEditTextHandler = null;
2981
3069
  let magnetMode = "none";
3070
+ let priceScaleMode = "linear";
3071
+ let priceScaleInverted = false;
3072
+ const PRICE_SCALE_LOG_FLOOR = 1e-9;
3073
+ const priceScaleTransform = (price) => priceScaleMode === "log" ? Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, price)) : price;
3074
+ const priceScaleUntransform = (value) => priceScaleMode === "log" ? Math.pow(10, value) : value;
3075
+ const percentBaselineForXStart = (xStartValue) => {
3076
+ if (priceScaleMode !== "percent") return null;
3077
+ const bar = data[clamp(Math.round(xStartValue), 0, Math.max(0, data.length - 1))];
3078
+ return bar && bar.c > 0 ? bar.c : null;
3079
+ };
3080
+ const formatPriceForScale = (price, percentBaseline) => {
3081
+ if (percentBaseline !== null) {
3082
+ const pct = (price / percentBaseline - 1) * 100;
3083
+ const rounded = Math.abs(pct) < 5e-3 ? 0 : pct;
3084
+ return `${rounded > 0 ? "+" : ""}${rounded.toFixed(2)}%`;
3085
+ }
3086
+ return formatPrice(price);
3087
+ };
2982
3088
  let magnetModifierActive = false;
2983
3089
  let shiftKeyActive = false;
2984
3090
  const MAGNET_WEAK_THRESHOLD_PX = 16;
@@ -4312,9 +4418,14 @@ function createChart(element, options = {}) {
4312
4418
  yMax
4313
4419
  };
4314
4420
  plotBottomForHit = fullChartBottom;
4421
+ const scaledYMin = priceScaleTransform(yMin);
4422
+ const scaledYRange = priceScaleTransform(yMax) - scaledYMin || 1;
4315
4423
  const yFromPrice = (price) => {
4316
- return chartBottom - (price - yMin) / yRange * chartHeight;
4424
+ const ratio = (priceScaleTransform(price) - scaledYMin) / scaledYRange;
4425
+ return priceScaleInverted ? chartTop + ratio * chartHeight : chartBottom - ratio * chartHeight;
4317
4426
  };
4427
+ const percentBaseline = percentBaselineForXStart(xStart);
4428
+ const formatAxisPrice = (price) => formatPriceForScale(price, percentBaseline);
4318
4429
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
4319
4430
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
4320
4431
  const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
@@ -4818,6 +4929,216 @@ function createChart(element, options = {}) {
4818
4929
  drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
4819
4930
  }
4820
4931
  }
4932
+ } else if (drawing.type === "ellipse") {
4933
+ const p0 = drawing.points[0];
4934
+ const p1 = drawing.points[1];
4935
+ if (p0 && p1) {
4936
+ const px0 = xFromDrawingPoint(p0);
4937
+ const px1 = xFromDrawingPoint(p1);
4938
+ const y0 = yFromPrice(p0.price);
4939
+ const y1 = yFromPrice(p1.price);
4940
+ const cx = (px0 + px1) / 2;
4941
+ const cy = (y0 + y1) / 2;
4942
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
4943
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
4944
+ ctx.save();
4945
+ ctx.globalAlpha = draft ? 0.6 : 1;
4946
+ ctx.beginPath();
4947
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
4948
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
4949
+ ctx.fill();
4950
+ ctx.lineWidth = Math.max(1, drawing.width);
4951
+ ctx.strokeStyle = drawing.color;
4952
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
4953
+ ctx.stroke();
4954
+ ctx.restore();
4955
+ handleAt(px0, y0, drawing.color);
4956
+ handleAt(px1, y1, drawing.color);
4957
+ handleAt(px0, y1, drawing.color);
4958
+ handleAt(px1, y0, drawing.color);
4959
+ if (drawing.label) {
4960
+ drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
4961
+ }
4962
+ }
4963
+ } else if (drawing.type === "parallel-channel") {
4964
+ const p0 = drawing.points[0];
4965
+ const p1 = drawing.points[1];
4966
+ const p2 = drawing.points[2];
4967
+ if (p0 && p1) {
4968
+ const x0 = xFromDrawingPoint(p0);
4969
+ const y0 = yFromPrice(p0.price);
4970
+ const x1 = xFromDrawingPoint(p1);
4971
+ const y1 = yFromPrice(p1.price);
4972
+ ctx.beginPath();
4973
+ ctx.moveTo(x0, y0);
4974
+ ctx.lineTo(x1, y1);
4975
+ ctx.stroke();
4976
+ handleAt(x0, y0, drawing.color);
4977
+ handleAt(x1, y1, drawing.color);
4978
+ if (p2) {
4979
+ const indexSpan = p1.index - p0.index;
4980
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
4981
+ const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
4982
+ const offset = p2.price - basePriceAtP2;
4983
+ const q0x = x0;
4984
+ const q0y = yFromPrice(p0.price + offset);
4985
+ const q1x = x1;
4986
+ const q1y = yFromPrice(p1.price + offset);
4987
+ ctx.save();
4988
+ ctx.globalAlpha = draft ? 0.5 : 1;
4989
+ ctx.fillStyle = hexToRgba(drawing.color, 0.08);
4990
+ ctx.beginPath();
4991
+ ctx.moveTo(x0, y0);
4992
+ ctx.lineTo(x1, y1);
4993
+ ctx.lineTo(q1x, q1y);
4994
+ ctx.lineTo(q0x, q0y);
4995
+ ctx.closePath();
4996
+ ctx.fill();
4997
+ ctx.restore();
4998
+ ctx.beginPath();
4999
+ ctx.moveTo(q0x, q0y);
5000
+ ctx.lineTo(q1x, q1y);
5001
+ ctx.stroke();
5002
+ ctx.save();
5003
+ ctx.setLineDash([4, 4]);
5004
+ ctx.globalAlpha = draft ? 0.5 : 0.7;
5005
+ ctx.beginPath();
5006
+ ctx.moveTo(x0, (y0 + q0y) / 2);
5007
+ ctx.lineTo(x1, (y1 + q1y) / 2);
5008
+ ctx.stroke();
5009
+ ctx.restore();
5010
+ handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
5011
+ }
5012
+ if (drawing.label) {
5013
+ drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
5014
+ }
5015
+ }
5016
+ } else if (drawing.type === "arrow") {
5017
+ const first = drawing.points[0];
5018
+ const second = drawing.points[1];
5019
+ if (first && second) {
5020
+ const x0 = xFromDrawingPoint(first);
5021
+ const y0 = yFromPrice(first.price);
5022
+ const x1 = xFromDrawingPoint(second);
5023
+ const y1 = yFromPrice(second.price);
5024
+ const angle = Math.atan2(y1 - y0, x1 - x0);
5025
+ const head = 5 + Math.max(1, drawing.width) * 3;
5026
+ const shaftX = x1 - Math.cos(angle) * head * 0.6;
5027
+ const shaftY = y1 - Math.sin(angle) * head * 0.6;
5028
+ ctx.beginPath();
5029
+ ctx.moveTo(x0, y0);
5030
+ ctx.lineTo(shaftX, shaftY);
5031
+ ctx.stroke();
5032
+ ctx.save();
5033
+ ctx.setLineDash([]);
5034
+ ctx.fillStyle = drawing.color;
5035
+ ctx.beginPath();
5036
+ ctx.moveTo(x1, y1);
5037
+ ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
5038
+ ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
5039
+ ctx.closePath();
5040
+ ctx.fill();
5041
+ ctx.restore();
5042
+ handleAt(x0, y0, drawing.color);
5043
+ handleAt(x1, y1, drawing.color);
5044
+ if (drawing.label) {
5045
+ const midX = (x0 + x1) / 2;
5046
+ const midY = (y0 + y1) / 2;
5047
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
5048
+ }
5049
+ }
5050
+ } else if (drawing.type === "brush") {
5051
+ const pts = drawing.points;
5052
+ if (pts.length >= 2) {
5053
+ ctx.save();
5054
+ ctx.lineJoin = "round";
5055
+ ctx.lineCap = "round";
5056
+ ctx.beginPath();
5057
+ const startX = xFromDrawingPoint(pts[0]);
5058
+ const startY = yFromPrice(pts[0].price);
5059
+ ctx.moveTo(startX, startY);
5060
+ if (pts.length === 2) {
5061
+ ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
5062
+ } else {
5063
+ for (let i = 1; i < pts.length - 1; i += 1) {
5064
+ const cxp = xFromDrawingPoint(pts[i]);
5065
+ const cyp = yFromPrice(pts[i].price);
5066
+ const nx = xFromDrawingPoint(pts[i + 1]);
5067
+ const ny = yFromPrice(pts[i + 1].price);
5068
+ ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
5069
+ }
5070
+ const last = pts[pts.length - 1];
5071
+ ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
5072
+ }
5073
+ ctx.stroke();
5074
+ ctx.restore();
5075
+ if (isSelected) {
5076
+ handleAt(startX, startY, drawing.color);
5077
+ const last = pts[pts.length - 1];
5078
+ handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
5079
+ }
5080
+ }
5081
+ } else if (drawing.type === "callout") {
5082
+ const anchor = drawing.points[0];
5083
+ const boxPoint = drawing.points[1];
5084
+ if (anchor && boxPoint) {
5085
+ const ax = xFromDrawingPoint(anchor);
5086
+ const ay = yFromPrice(anchor.price);
5087
+ const bx = xFromDrawingPoint(boxPoint);
5088
+ const by = yFromPrice(boxPoint.price);
5089
+ const fontSize = Math.max(6, drawing.fontSize);
5090
+ const prevFont = ctx.font;
5091
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
5092
+ const rawLabel = drawing.label ?? "";
5093
+ const isPlaceholder = rawLabel.trim().length === 0;
5094
+ const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
5095
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
5096
+ const padX = 10;
5097
+ const padY = 7;
5098
+ const lineH = Math.round(fontSize * 1.35);
5099
+ const blockW = textW + padX * 2;
5100
+ const blockH = lines.length * lineH + padY * 2;
5101
+ const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
5102
+ const edgeY = Math.max(by, Math.min(by + blockH, ay));
5103
+ const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
5104
+ if (!insideBox) {
5105
+ ctx.save();
5106
+ ctx.setLineDash([]);
5107
+ ctx.lineWidth = Math.max(1, drawing.width);
5108
+ ctx.beginPath();
5109
+ ctx.moveTo(edgeX, edgeY);
5110
+ ctx.lineTo(ax, ay);
5111
+ ctx.stroke();
5112
+ const angle = Math.atan2(ay - edgeY, ax - edgeX);
5113
+ const head = 7;
5114
+ ctx.fillStyle = drawing.color;
5115
+ ctx.beginPath();
5116
+ ctx.moveTo(ax, ay);
5117
+ ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
5118
+ ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
5119
+ ctx.closePath();
5120
+ ctx.fill();
5121
+ ctx.restore();
5122
+ }
5123
+ ctx.save();
5124
+ ctx.setLineDash([]);
5125
+ ctx.fillStyle = hexToRgba(drawing.color, 0.16);
5126
+ fillRoundedRect(bx, by, blockW, blockH, 6);
5127
+ ctx.strokeStyle = drawing.color;
5128
+ ctx.lineWidth = Math.max(1, drawing.width);
5129
+ strokeRoundedRect(bx, by, blockW, blockH, 6);
5130
+ ctx.fillStyle = drawing.color;
5131
+ ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
5132
+ ctx.textAlign = "left";
5133
+ ctx.textBaseline = "middle";
5134
+ lines.forEach((line, lineIndex) => {
5135
+ ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
5136
+ });
5137
+ ctx.restore();
5138
+ ctx.font = prevFont;
5139
+ handleAt(ax, ay, drawing.color);
5140
+ handleAt(bx, by, drawing.color);
5141
+ }
4821
5142
  } else if (drawing.type === "price-range") {
4822
5143
  const p0 = drawing.points[0];
4823
5144
  const p1 = drawing.points[1];
@@ -5011,19 +5332,40 @@ function createChart(element, options = {}) {
5011
5332
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
5012
5333
  const gridColor = grid.color ?? mergedOptions.gridColor;
5013
5334
  const priceTicks = [];
5014
- {
5015
- const targetStep = yRange / Math.max(1, yTicks);
5016
- let step;
5017
- if (targetStep > 0 && Number.isFinite(targetStep)) {
5018
- const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5019
- const frac = targetStep / base;
5020
- step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5021
- const instrumentTick = getConfiguredTickSize();
5022
- if (instrumentTick > 0 && step < instrumentTick * 1e6) {
5023
- step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5335
+ const pickNiceStep = (targetStep) => {
5336
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return 1;
5337
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5338
+ const frac = targetStep / base;
5339
+ return (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5340
+ };
5341
+ if (percentBaseline !== null) {
5342
+ const pctMin = (yMin / percentBaseline - 1) * 100;
5343
+ const pctMax = (yMax / percentBaseline - 1) * 100;
5344
+ const step = pickNiceStep((pctMax - pctMin) / Math.max(1, yTicks));
5345
+ const firstTick = Math.ceil(pctMin / step) * step;
5346
+ for (let i = 0; ; i += 1) {
5347
+ const pct = firstTick + i * step;
5348
+ if (pct > pctMax + step * 1e-6) break;
5349
+ priceTicks.push(percentBaseline * (1 + pct / 100));
5350
+ if (priceTicks.length > 200) break;
5351
+ }
5352
+ } else if (priceScaleMode === "log" && yMin > 0 && yMax / yMin > 4) {
5353
+ const mantissas = Math.log10(yMax / yMin) > 8 ? [1] : [1, 2, 5];
5354
+ const firstDecade = Math.floor(Math.log10(Math.max(PRICE_SCALE_LOG_FLOOR, yMin)));
5355
+ const lastDecade = Math.ceil(Math.log10(yMax));
5356
+ for (let decade = firstDecade; decade <= lastDecade; decade += 1) {
5357
+ for (const mantissa of mantissas) {
5358
+ const price = mantissa * Math.pow(10, decade);
5359
+ if (price >= yMin && price <= yMax) priceTicks.push(price);
5360
+ if (priceTicks.length > 200) break;
5024
5361
  }
5025
- } else {
5026
- step = 1;
5362
+ }
5363
+ } else {
5364
+ const targetStep = yRange / Math.max(1, yTicks);
5365
+ let step = pickNiceStep(targetStep);
5366
+ const instrumentTick = getConfiguredTickSize();
5367
+ if (targetStep > 0 && Number.isFinite(targetStep) && instrumentTick > 0 && step < instrumentTick * 1e6) {
5368
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
5027
5369
  }
5028
5370
  const firstTick = Math.ceil(yMin / step) * step;
5029
5371
  for (let i = 0; ; i += 1) {
@@ -5786,7 +6128,7 @@ function createChart(element, options = {}) {
5786
6128
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
5787
6129
  for (const price of priceTicks) {
5788
6130
  const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
5789
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6131
+ drawText(formatAxisPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
5790
6132
  }
5791
6133
  ctx.font = prevFont;
5792
6134
  }
@@ -5861,7 +6203,7 @@ function createChart(element, options = {}) {
5861
6203
  ...ticker.showCountdownInLabel ? [getCountdownText()] : []
5862
6204
  ].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
5863
6205
  addPriceAxisLabel({
5864
- text: formatPrice(tickerPrice),
6206
+ text: formatAxisPrice(tickerPrice),
5865
6207
  ...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
5866
6208
  subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
5867
6209
  ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
@@ -5888,7 +6230,7 @@ function createChart(element, options = {}) {
5888
6230
  const previousClose = previousCloseCandidate;
5889
6231
  drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
5890
6232
  addPriceAxisLabel({
5891
- text: `PDC ${formatPrice(previousClose)}`,
6233
+ text: `PDC ${formatAxisPrice(previousClose)}`,
5892
6234
  price: previousClose,
5893
6235
  backgroundColor: labels.backgroundColor,
5894
6236
  textColor: labels.mutedTextColor,
@@ -5907,7 +6249,7 @@ function createChart(element, options = {}) {
5907
6249
  if (point.l < visibleLow) visibleLow = point.l;
5908
6250
  }
5909
6251
  addPriceAxisLabel({
5910
- text: `H ${formatPrice(visibleHigh)}`,
6252
+ text: `H ${formatAxisPrice(visibleHigh)}`,
5911
6253
  price: visibleHigh,
5912
6254
  backgroundColor: labels.backgroundColor,
5913
6255
  textColor: labels.textColor,
@@ -5915,7 +6257,7 @@ function createChart(element, options = {}) {
5915
6257
  priority: 40
5916
6258
  });
5917
6259
  addPriceAxisLabel({
5918
- text: `L ${formatPrice(visibleLow)}`,
6260
+ text: `L ${formatAxisPrice(visibleLow)}`,
5919
6261
  price: visibleLow,
5920
6262
  backgroundColor: labels.backgroundColor,
5921
6263
  textColor: labels.textColor,
@@ -5927,7 +6269,7 @@ function createChart(element, options = {}) {
5927
6269
  if (Number.isFinite(labels.bidPrice)) {
5928
6270
  drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
5929
6271
  addPriceAxisLabel({
5930
- text: `B ${formatPrice(labels.bidPrice)}`,
6272
+ text: `B ${formatAxisPrice(labels.bidPrice)}`,
5931
6273
  price: labels.bidPrice,
5932
6274
  backgroundColor: labels.bidColor,
5933
6275
  textColor: "#0b1220",
@@ -5938,7 +6280,7 @@ function createChart(element, options = {}) {
5938
6280
  if (Number.isFinite(labels.askPrice)) {
5939
6281
  drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
5940
6282
  addPriceAxisLabel({
5941
- text: `A ${formatPrice(labels.askPrice)}`,
6283
+ text: `A ${formatAxisPrice(labels.askPrice)}`,
5942
6284
  price: labels.askPrice,
5943
6285
  backgroundColor: labels.askColor,
5944
6286
  textColor: "#0b1220",
@@ -6199,8 +6541,11 @@ function createChart(element, options = {}) {
6199
6541
  ctx.restore();
6200
6542
  };
6201
6543
  if (crosshair.showPriceLabel) {
6202
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
6203
- const priceText = formatPrice(hoverPrice);
6544
+ const scaledYMin = priceScaleTransform(yMin);
6545
+ const scaledYRange = priceScaleTransform(yMin + yRange) - scaledYMin || 1;
6546
+ const hoverRatio = priceScaleInverted ? (cy - chartTop) / chartHeight : (chartBottom - cy) / chartHeight;
6547
+ const hoverPrice = priceScaleUntransform(scaledYMin + hoverRatio * scaledYRange);
6548
+ const priceText = formatPriceForScale(hoverPrice, percentBaselineForXStart(xStart));
6204
6549
  const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
6205
6550
  const priceX = getRightAxisLabelX(chartRight);
6206
6551
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
@@ -6368,8 +6713,8 @@ function createChart(element, options = {}) {
6368
6713
  const maxRange = bounds.hardSpan;
6369
6714
  const nextRange = clamp(currentRange * factor, minRange, maxRange);
6370
6715
  const anchorRatio = clamp((anchorY - drawState.chartTop) / drawState.chartHeight, 0, 1);
6371
- const anchorPrice = currentMax - anchorRatio * currentRange;
6372
- const nextMax = anchorPrice + anchorRatio * nextRange;
6716
+ const anchorPrice = priceScaleInverted ? currentMin + anchorRatio * currentRange : currentMax - anchorRatio * currentRange;
6717
+ const nextMax = priceScaleInverted ? anchorPrice + (1 - anchorRatio) * nextRange : anchorPrice + anchorRatio * nextRange;
6373
6718
  const nextMin = nextMax - nextRange;
6374
6719
  const clamped = clampYRange(nextMin, nextMax);
6375
6720
  yMinOverride = clamped.min;
@@ -6393,7 +6738,7 @@ function createChart(element, options = {}) {
6393
6738
  const currentMin = yMinOverride ?? drawState.yMin;
6394
6739
  const currentMax = yMaxOverride ?? drawState.yMax;
6395
6740
  const range = currentMax - currentMin || 1;
6396
- const priceShift = deltaY / drawState.chartHeight * range;
6741
+ const priceShift = deltaY / drawState.chartHeight * range * (priceScaleInverted ? -1 : 1);
6397
6742
  const clamped = clampYRange(currentMin + priceShift, currentMax + priceShift);
6398
6743
  yMinOverride = clamped.min;
6399
6744
  yMaxOverride = clamped.max;
@@ -6626,8 +6971,14 @@ function createChart(element, options = {}) {
6626
6971
  if (!drawState) {
6627
6972
  return 0;
6628
6973
  }
6629
- const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
6630
- return drawState.yMin + ratio * (drawState.yMax - drawState.yMin);
6974
+ const ratio = clamp(
6975
+ priceScaleInverted ? (y - drawState.chartTop) / drawState.chartHeight : (drawState.chartBottom - y) / drawState.chartHeight,
6976
+ 0,
6977
+ 1
6978
+ );
6979
+ const scaledMin = priceScaleTransform(drawState.yMin);
6980
+ const scaledMax = priceScaleTransform(drawState.yMax);
6981
+ return priceScaleUntransform(scaledMin + ratio * (scaledMax - scaledMin));
6631
6982
  };
6632
6983
  const applyMagnet = (y, index, price) => {
6633
6984
  const effective = magnetModifierActive ? "strong" : magnetMode;
@@ -6699,8 +7050,10 @@ function createChart(element, options = {}) {
6699
7050
  };
6700
7051
  const canvasYFromDrawingPrice = (price) => {
6701
7052
  if (!drawState) return 0;
6702
- const range = drawState.yMax - drawState.yMin || 1;
6703
- return drawState.chartBottom - (price - drawState.yMin) / range * drawState.chartHeight;
7053
+ const scaledMin = priceScaleTransform(drawState.yMin);
7054
+ const scaledRange = priceScaleTransform(drawState.yMax) - scaledMin || 1;
7055
+ const ratio = (priceScaleTransform(price) - scaledMin) / scaledRange;
7056
+ return priceScaleInverted ? drawState.chartTop + ratio * drawState.chartHeight : drawState.chartBottom - ratio * drawState.chartHeight;
6704
7057
  };
6705
7058
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
6706
7059
  const dx = x2 - x1;
@@ -6872,6 +7225,126 @@ function createChart(element, options = {}) {
6872
7225
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
6873
7226
  return { drawing, target: "line" };
6874
7227
  }
7228
+ } else if (drawing.type === "ellipse") {
7229
+ const p0 = drawing.points[0];
7230
+ const p1 = drawing.points[1];
7231
+ if (!p0 || !p1) continue;
7232
+ const px0 = canvasXFromDrawingPoint(p0);
7233
+ const px1 = canvasXFromDrawingPoint(p1);
7234
+ const y0 = canvasYFromDrawingPrice(p0.price);
7235
+ const y1 = canvasYFromDrawingPrice(p1.price);
7236
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7237
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7238
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7239
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7240
+ const cx = (px0 + px1) / 2;
7241
+ const cy = (y0 + y1) / 2;
7242
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7243
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7244
+ const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7245
+ if (norm <= 1) {
7246
+ return { drawing, target: "line" };
7247
+ }
7248
+ } else if (drawing.type === "parallel-channel") {
7249
+ const p0 = drawing.points[0];
7250
+ const p1 = drawing.points[1];
7251
+ const p2 = drawing.points[2];
7252
+ if (!p0 || !p1) continue;
7253
+ const x0 = canvasXFromDrawingPoint(p0);
7254
+ const y0 = canvasYFromDrawingPrice(p0.price);
7255
+ const x1 = canvasXFromDrawingPoint(p1);
7256
+ const y1 = canvasYFromDrawingPrice(p1.price);
7257
+ if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7258
+ if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7259
+ if (p2) {
7260
+ const x2 = canvasXFromDrawingPoint(p2);
7261
+ const y2 = canvasYFromDrawingPrice(p2.price);
7262
+ if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7263
+ const indexSpan = p1.index - p0.index;
7264
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7265
+ const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7266
+ const q0y = canvasYFromDrawingPrice(p0.price + offset);
7267
+ const q1y = canvasYFromDrawingPrice(p1.price + offset);
7268
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7269
+ return { drawing, target: "line" };
7270
+ }
7271
+ const minX = Math.min(x0, x1);
7272
+ const maxX = Math.max(x0, x1);
7273
+ if (x >= minX && x <= maxX && maxX > minX) {
7274
+ const t = (x - x0) / (x1 - x0);
7275
+ const baseY = y0 + t * (y1 - y0);
7276
+ const parY = q0y + t * (q1y - q0y);
7277
+ if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
7278
+ return { drawing, target: "line" };
7279
+ }
7280
+ }
7281
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7282
+ return { drawing, target: "line" };
7283
+ }
7284
+ } else if (drawing.type === "arrow") {
7285
+ const first = drawing.points[0];
7286
+ const second = drawing.points[1];
7287
+ if (!first || !second) continue;
7288
+ const x1 = canvasXFromDrawingPoint(first);
7289
+ const y1 = canvasYFromDrawingPrice(first.price);
7290
+ const x2 = canvasXFromDrawingPoint(second);
7291
+ const y2 = canvasYFromDrawingPrice(second.price);
7292
+ if (Math.hypot(x - x1, y - y1) <= 8) {
7293
+ return { drawing, target: "handle", pointIndex: 0 };
7294
+ }
7295
+ if (Math.hypot(x - x2, y - y2) <= 8) {
7296
+ return { drawing, target: "handle", pointIndex: 1 };
7297
+ }
7298
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7299
+ return { drawing, target: "line" };
7300
+ }
7301
+ } else if (drawing.type === "brush") {
7302
+ const pts = drawing.points;
7303
+ if (pts.length < 2) continue;
7304
+ let hitBody = false;
7305
+ let prevX = canvasXFromDrawingPoint(pts[0]);
7306
+ let prevY = canvasYFromDrawingPrice(pts[0].price);
7307
+ for (let i = 1; i < pts.length; i += 1) {
7308
+ const nextX = canvasXFromDrawingPoint(pts[i]);
7309
+ const nextY = canvasYFromDrawingPrice(pts[i].price);
7310
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7311
+ hitBody = true;
7312
+ break;
7313
+ }
7314
+ prevX = nextX;
7315
+ prevY = nextY;
7316
+ }
7317
+ if (hitBody) {
7318
+ return { drawing, target: "line" };
7319
+ }
7320
+ } else if (drawing.type === "callout") {
7321
+ const anchor = drawing.points[0];
7322
+ const boxPoint = drawing.points[1];
7323
+ if (!anchor || !boxPoint) continue;
7324
+ const ax = canvasXFromDrawingPoint(anchor);
7325
+ const ay = canvasYFromDrawingPrice(anchor.price);
7326
+ const bx = canvasXFromDrawingPoint(boxPoint);
7327
+ const by = canvasYFromDrawingPrice(boxPoint.price);
7328
+ if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7329
+ if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7330
+ const fontSize = Math.max(6, drawing.fontSize);
7331
+ const prevFont = ctx.font;
7332
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
7333
+ const rawLabel = drawing.label ?? "";
7334
+ const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
7335
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
7336
+ ctx.font = prevFont;
7337
+ const padX = 10;
7338
+ const padY = 7;
7339
+ const lineH = Math.round(fontSize * 1.35);
7340
+ const blockW = textW + padX * 2;
7341
+ const blockH = lines.length * lineH + padY * 2;
7342
+ if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7343
+ return { drawing, target: "line" };
7344
+ }
7345
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7346
+ return { drawing, target: "line" };
7347
+ }
6875
7348
  } else if (drawing.type === "price-range") {
6876
7349
  const p0 = drawing.points[0];
6877
7350
  const p1 = drawing.points[1];
@@ -7100,6 +7573,128 @@ function createChart(element, options = {}) {
7100
7573
  scheduleDraw();
7101
7574
  return true;
7102
7575
  }
7576
+ if (activeDrawingTool === "arrow") {
7577
+ if (draftDrawing?.type === "arrow") {
7578
+ const anchor = draftDrawing.points[0];
7579
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
7580
+ const completed = normalizeDrawingState({
7581
+ ...serializeDrawing(draftDrawing),
7582
+ points: [anchor, endPoint]
7583
+ });
7584
+ drawings.push(completed);
7585
+ draftDrawing = null;
7586
+ activeDrawingTool = null;
7587
+ emitDrawingsChange();
7588
+ scheduleDraw();
7589
+ return true;
7590
+ }
7591
+ const defaults = getDrawingToolDefaults("arrow");
7592
+ draftDrawing = normalizeDrawingState({
7593
+ type: "arrow",
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 === "parallel-channel") {
7603
+ if (draftDrawing?.type === "parallel-channel") {
7604
+ if (draftDrawing.points.length < 3) {
7605
+ draftDrawing = normalizeDrawingState({
7606
+ ...serializeDrawing(draftDrawing),
7607
+ points: [...draftDrawing.points.slice(0, -1), point, point]
7608
+ });
7609
+ scheduleDraw();
7610
+ return true;
7611
+ }
7612
+ const completed = normalizeDrawingState({
7613
+ ...serializeDrawing(draftDrawing),
7614
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
7615
+ });
7616
+ drawings.push(completed);
7617
+ draftDrawing = null;
7618
+ activeDrawingTool = null;
7619
+ emitDrawingsChange();
7620
+ scheduleDraw();
7621
+ return true;
7622
+ }
7623
+ const defaults = getDrawingToolDefaults("parallel-channel");
7624
+ draftDrawing = normalizeDrawingState({
7625
+ type: "parallel-channel",
7626
+ points: [point, point],
7627
+ color: defaults.color ?? "#2563eb",
7628
+ style: defaults.style ?? "solid",
7629
+ width: defaults.width ?? 2
7630
+ });
7631
+ scheduleDraw();
7632
+ return true;
7633
+ }
7634
+ if (activeDrawingTool === "ellipse") {
7635
+ if (draftDrawing?.type === "ellipse") {
7636
+ const completed = normalizeDrawingState({
7637
+ ...serializeDrawing(draftDrawing),
7638
+ points: [draftDrawing.points[0], point]
7639
+ });
7640
+ drawings.push(completed);
7641
+ draftDrawing = null;
7642
+ activeDrawingTool = null;
7643
+ emitDrawingsChange();
7644
+ scheduleDraw();
7645
+ return true;
7646
+ }
7647
+ const defaults = getDrawingToolDefaults("ellipse");
7648
+ draftDrawing = normalizeDrawingState({
7649
+ type: "ellipse",
7650
+ points: [point, point],
7651
+ color: defaults.color ?? "#2962ff",
7652
+ style: defaults.style ?? "solid",
7653
+ width: defaults.width ?? 1
7654
+ });
7655
+ scheduleDraw();
7656
+ return true;
7657
+ }
7658
+ if (activeDrawingTool === "callout") {
7659
+ if (draftDrawing?.type === "callout") {
7660
+ const completed = normalizeDrawingState({
7661
+ ...serializeDrawing(draftDrawing),
7662
+ points: [draftDrawing.points[0], point]
7663
+ });
7664
+ drawings.push(completed);
7665
+ draftDrawing = null;
7666
+ activeDrawingTool = null;
7667
+ selectedDrawingId = completed.id;
7668
+ emitDrawingsChange();
7669
+ scheduleDraw();
7670
+ drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
7671
+ return true;
7672
+ }
7673
+ const defaults = getDrawingToolDefaults("callout");
7674
+ draftDrawing = normalizeDrawingState({
7675
+ type: "callout",
7676
+ points: [point, point],
7677
+ color: defaults.color ?? "#2962ff",
7678
+ style: defaults.style ?? "solid",
7679
+ width: defaults.width ?? 1,
7680
+ ...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
7681
+ label: ""
7682
+ });
7683
+ scheduleDraw();
7684
+ return true;
7685
+ }
7686
+ if (activeDrawingTool === "brush") {
7687
+ const defaults = getDrawingToolDefaults("brush");
7688
+ draftDrawing = normalizeDrawingState({
7689
+ type: "brush",
7690
+ points: [point],
7691
+ color: defaults.color ?? "#f59e0b",
7692
+ style: defaults.style ?? "solid",
7693
+ width: defaults.width ?? 2
7694
+ });
7695
+ scheduleDraw();
7696
+ return true;
7697
+ }
7103
7698
  if (activeDrawingTool === "fib-retracement") {
7104
7699
  if (draftDrawing?.type === "fib-retracement") {
7105
7700
  const completed = normalizeDrawingState({
@@ -7303,7 +7898,7 @@ function createChart(element, options = {}) {
7303
7898
  }
7304
7899
  return { ...drawing, points: pts };
7305
7900
  }
7306
- if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
7901
+ if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
7307
7902
  const pts = drawing.points.map((point) => ({ ...point }));
7308
7903
  const p0 = pts[0];
7309
7904
  const p1 = pts[1];
@@ -7322,7 +7917,7 @@ function createChart(element, options = {}) {
7322
7917
  }
7323
7918
  return { ...drawing, points: pts };
7324
7919
  }
7325
- if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
7920
+ if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
7326
7921
  const pointIndex = drawingDragState.pointIndex ?? 0;
7327
7922
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
7328
7923
  const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
@@ -7572,6 +8167,10 @@ function createChart(element, options = {}) {
7572
8167
  if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
7573
8168
  setCrosshairPoint(null);
7574
8169
  canvas.style.cursor = "crosshair";
8170
+ if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
8171
+ activePointerId = event.pointerId;
8172
+ capturePointer(event.pointerId);
8173
+ }
7575
8174
  return;
7576
8175
  }
7577
8176
  isDragging = true;
@@ -7656,9 +8255,30 @@ function createChart(element, options = {}) {
7656
8255
  setCrosshairPoint(null);
7657
8256
  return;
7658
8257
  }
7659
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
8258
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8259
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
8260
+ return;
8261
+ }
8262
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
8263
+ if (nextPoint) {
8264
+ const last = draftDrawing.points[draftDrawing.points.length - 1];
8265
+ const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
8266
+ const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
8267
+ if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
8268
+ draftDrawing = {
8269
+ ...draftDrawing,
8270
+ points: [...draftDrawing.points, nextPoint]
8271
+ };
8272
+ }
8273
+ canvas.style.cursor = "crosshair";
8274
+ setCrosshairPoint(null);
8275
+ scheduleDraw();
8276
+ }
8277
+ return;
8278
+ }
8279
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
7660
8280
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
7661
- if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
8281
+ if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
7662
8282
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
7663
8283
  }
7664
8284
  if (nextPoint) {
@@ -7895,6 +8515,25 @@ function createChart(element, options = {}) {
7895
8515
  scheduleDraw();
7896
8516
  return;
7897
8517
  }
8518
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8519
+ if (draftDrawing.points.length >= 2) {
8520
+ const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
8521
+ drawings.push(completed);
8522
+ draftDrawing = null;
8523
+ activeDrawingTool = null;
8524
+ canvas.style.cursor = "default";
8525
+ emitDrawingsChange();
8526
+ } else {
8527
+ draftDrawing = null;
8528
+ canvas.style.cursor = "crosshair";
8529
+ }
8530
+ isDragging = false;
8531
+ dragMode = null;
8532
+ activePointerId = null;
8533
+ pointerDownInfo = null;
8534
+ scheduleDraw();
8535
+ return;
8536
+ }
7898
8537
  if (orderDragState) {
7899
8538
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7900
8539
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -8038,7 +8677,7 @@ function createChart(element, options = {}) {
8038
8677
  x: point.x,
8039
8678
  y: point.y
8040
8679
  };
8041
- if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
8680
+ if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
8042
8681
  drawingEditTextHandler?.(payload);
8043
8682
  } else {
8044
8683
  drawingDoubleClickHandler?.(payload);
@@ -8428,6 +9067,16 @@ function createChart(element, options = {}) {
8428
9067
  magnetMode = mode;
8429
9068
  };
8430
9069
  const getMagnetMode = () => magnetMode;
9070
+ const setPriceScale = (options2) => {
9071
+ if (options2.mode === "linear" || options2.mode === "log" || options2.mode === "percent") {
9072
+ priceScaleMode = options2.mode;
9073
+ }
9074
+ if (typeof options2.inverted === "boolean") {
9075
+ priceScaleInverted = options2.inverted;
9076
+ }
9077
+ scheduleDraw();
9078
+ };
9079
+ const getPriceScale = () => ({ mode: priceScaleMode, inverted: priceScaleInverted });
8431
9080
  const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
8432
9081
  const setDrawings = (nextDrawings) => {
8433
9082
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
@@ -8485,6 +9134,55 @@ function createChart(element, options = {}) {
8485
9134
  const onDrawingHover = (handler) => {
8486
9135
  drawingHoverHandler = handler;
8487
9136
  };
9137
+ const saveState = () => {
9138
+ const drawingDefaults = {};
9139
+ for (const [tool, defaults] of drawingToolDefaults) {
9140
+ drawingDefaults[tool] = { ...defaults };
9141
+ }
9142
+ return {
9143
+ version: 1,
9144
+ chartType: getChartType(),
9145
+ viewport: getViewport(),
9146
+ drawings: getDrawings(),
9147
+ indicators: getIndicators(),
9148
+ magnetMode: getMagnetMode(),
9149
+ priceScale: getPriceScale(),
9150
+ drawingDefaults
9151
+ };
9152
+ };
9153
+ const loadState = (state) => {
9154
+ if (!state || typeof state !== "object") {
9155
+ return;
9156
+ }
9157
+ if (typeof state.chartType === "string") {
9158
+ setChartType(state.chartType);
9159
+ }
9160
+ if (Array.isArray(state.indicators)) {
9161
+ setIndicators(state.indicators);
9162
+ }
9163
+ if (Array.isArray(state.drawings)) {
9164
+ setDrawings(state.drawings);
9165
+ }
9166
+ if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
9167
+ setMagnetMode(state.magnetMode);
9168
+ }
9169
+ if (state.priceScale && typeof state.priceScale === "object") {
9170
+ setPriceScale(state.priceScale);
9171
+ }
9172
+ if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
9173
+ for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
9174
+ if (defaults && typeof defaults === "object") {
9175
+ setDrawingDefaults(tool, defaults);
9176
+ }
9177
+ }
9178
+ }
9179
+ if (state.viewport && typeof state.viewport === "object") {
9180
+ setViewport(state.viewport);
9181
+ }
9182
+ };
9183
+ const takeScreenshot = (options2 = {}) => {
9184
+ return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
9185
+ };
8488
9186
  const destroy = () => {
8489
9187
  cancelKineticPan();
8490
9188
  if (smoothingRafId !== null) {
@@ -8565,6 +9263,8 @@ function createChart(element, options = {}) {
8565
9263
  onDrawingEditText,
8566
9264
  setMagnetMode,
8567
9265
  getMagnetMode,
9266
+ setPriceScale,
9267
+ getPriceScale,
8568
9268
  cancelDrawing,
8569
9269
  setTradeMarkers,
8570
9270
  setSelectedDrawing,
@@ -8580,6 +9280,9 @@ function createChart(element, options = {}) {
8580
9280
  updateIndicator,
8581
9281
  removeIndicator,
8582
9282
  setIndicators,
9283
+ saveState,
9284
+ loadState,
9285
+ takeScreenshot,
8583
9286
  resize,
8584
9287
  destroy
8585
9288
  };
@@ -8588,7 +9291,9 @@ function createChart(element, options = {}) {
8588
9291
  0 && (module.exports = {
8589
9292
  FIB_DEFAULT_PALETTE,
8590
9293
  POSITION_DEFAULT_COLORS,
9294
+ SCRIPT_SOURCE_INPUT_VALUES,
8591
9295
  compileScriptIndicator,
8592
9296
  createChart,
9297
+ extractScriptInputs,
8593
9298
  validateScriptSource
8594
9299
  });