hyperprop-charting-library 0.1.136 → 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];
@@ -5191,19 +5300,40 @@ function createChart(element, options = {}) {
5191
5300
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
5192
5301
  const gridColor = grid.color ?? mergedOptions.gridColor;
5193
5302
  const priceTicks = [];
5194
- {
5195
- const targetStep = yRange / Math.max(1, yTicks);
5196
- let step;
5197
- if (targetStep > 0 && Number.isFinite(targetStep)) {
5198
- const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
5199
- const frac = targetStep / base;
5200
- step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
5201
- const instrumentTick = getConfiguredTickSize();
5202
- if (instrumentTick > 0 && step < instrumentTick * 1e6) {
5203
- step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
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;
5204
5329
  }
5205
- } else {
5206
- 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);
5207
5337
  }
5208
5338
  const firstTick = Math.ceil(yMin / step) * step;
5209
5339
  for (let i = 0; ; i += 1) {
@@ -5966,7 +6096,7 @@ function createChart(element, options = {}) {
5966
6096
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
5967
6097
  for (const price of priceTicks) {
5968
6098
  const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
5969
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
6099
+ drawText(formatAxisPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
5970
6100
  }
5971
6101
  ctx.font = prevFont;
5972
6102
  }
@@ -6041,7 +6171,7 @@ function createChart(element, options = {}) {
6041
6171
  ...ticker.showCountdownInLabel ? [getCountdownText()] : []
6042
6172
  ].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
6043
6173
  addPriceAxisLabel({
6044
- text: formatPrice(tickerPrice),
6174
+ text: formatAxisPrice(tickerPrice),
6045
6175
  ...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
6046
6176
  subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
6047
6177
  ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
@@ -6068,7 +6198,7 @@ function createChart(element, options = {}) {
6068
6198
  const previousClose = previousCloseCandidate;
6069
6199
  drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
6070
6200
  addPriceAxisLabel({
6071
- text: `PDC ${formatPrice(previousClose)}`,
6201
+ text: `PDC ${formatAxisPrice(previousClose)}`,
6072
6202
  price: previousClose,
6073
6203
  backgroundColor: labels.backgroundColor,
6074
6204
  textColor: labels.mutedTextColor,
@@ -6087,7 +6217,7 @@ function createChart(element, options = {}) {
6087
6217
  if (point.l < visibleLow) visibleLow = point.l;
6088
6218
  }
6089
6219
  addPriceAxisLabel({
6090
- text: `H ${formatPrice(visibleHigh)}`,
6220
+ text: `H ${formatAxisPrice(visibleHigh)}`,
6091
6221
  price: visibleHigh,
6092
6222
  backgroundColor: labels.backgroundColor,
6093
6223
  textColor: labels.textColor,
@@ -6095,7 +6225,7 @@ function createChart(element, options = {}) {
6095
6225
  priority: 40
6096
6226
  });
6097
6227
  addPriceAxisLabel({
6098
- text: `L ${formatPrice(visibleLow)}`,
6228
+ text: `L ${formatAxisPrice(visibleLow)}`,
6099
6229
  price: visibleLow,
6100
6230
  backgroundColor: labels.backgroundColor,
6101
6231
  textColor: labels.textColor,
@@ -6107,7 +6237,7 @@ function createChart(element, options = {}) {
6107
6237
  if (Number.isFinite(labels.bidPrice)) {
6108
6238
  drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
6109
6239
  addPriceAxisLabel({
6110
- text: `B ${formatPrice(labels.bidPrice)}`,
6240
+ text: `B ${formatAxisPrice(labels.bidPrice)}`,
6111
6241
  price: labels.bidPrice,
6112
6242
  backgroundColor: labels.bidColor,
6113
6243
  textColor: "#0b1220",
@@ -6118,7 +6248,7 @@ function createChart(element, options = {}) {
6118
6248
  if (Number.isFinite(labels.askPrice)) {
6119
6249
  drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
6120
6250
  addPriceAxisLabel({
6121
- text: `A ${formatPrice(labels.askPrice)}`,
6251
+ text: `A ${formatAxisPrice(labels.askPrice)}`,
6122
6252
  price: labels.askPrice,
6123
6253
  backgroundColor: labels.askColor,
6124
6254
  textColor: "#0b1220",
@@ -6379,8 +6509,11 @@ function createChart(element, options = {}) {
6379
6509
  ctx.restore();
6380
6510
  };
6381
6511
  if (crosshair.showPriceLabel) {
6382
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
6383
- 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));
6384
6517
  const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
6385
6518
  const priceX = getRightAxisLabelX(chartRight);
6386
6519
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
@@ -6548,8 +6681,8 @@ function createChart(element, options = {}) {
6548
6681
  const maxRange = bounds.hardSpan;
6549
6682
  const nextRange = clamp(currentRange * factor, minRange, maxRange);
6550
6683
  const anchorRatio = clamp((anchorY - drawState.chartTop) / drawState.chartHeight, 0, 1);
6551
- const anchorPrice = currentMax - anchorRatio * currentRange;
6552
- 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;
6553
6686
  const nextMin = nextMax - nextRange;
6554
6687
  const clamped = clampYRange(nextMin, nextMax);
6555
6688
  yMinOverride = clamped.min;
@@ -6573,7 +6706,7 @@ function createChart(element, options = {}) {
6573
6706
  const currentMin = yMinOverride ?? drawState.yMin;
6574
6707
  const currentMax = yMaxOverride ?? drawState.yMax;
6575
6708
  const range = currentMax - currentMin || 1;
6576
- const priceShift = deltaY / drawState.chartHeight * range;
6709
+ const priceShift = deltaY / drawState.chartHeight * range * (priceScaleInverted ? -1 : 1);
6577
6710
  const clamped = clampYRange(currentMin + priceShift, currentMax + priceShift);
6578
6711
  yMinOverride = clamped.min;
6579
6712
  yMaxOverride = clamped.max;
@@ -6806,8 +6939,14 @@ function createChart(element, options = {}) {
6806
6939
  if (!drawState) {
6807
6940
  return 0;
6808
6941
  }
6809
- const ratio = clamp((drawState.chartBottom - y) / drawState.chartHeight, 0, 1);
6810
- 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));
6811
6950
  };
6812
6951
  const applyMagnet = (y, index, price) => {
6813
6952
  const effective = magnetModifierActive ? "strong" : magnetMode;
@@ -6879,8 +7018,10 @@ function createChart(element, options = {}) {
6879
7018
  };
6880
7019
  const canvasYFromDrawingPrice = (price) => {
6881
7020
  if (!drawState) return 0;
6882
- const range = drawState.yMax - drawState.yMin || 1;
6883
- 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;
6884
7025
  };
6885
7026
  const distanceToSegment = (x, y, x1, y1, x2, y2) => {
6886
7027
  const dx = x2 - x1;
@@ -8894,6 +9035,16 @@ function createChart(element, options = {}) {
8894
9035
  magnetMode = mode;
8895
9036
  };
8896
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 });
8897
9048
  const getDrawings = () => drawings.map((drawing) => serializeDrawing(drawing));
8898
9049
  const setDrawings = (nextDrawings) => {
8899
9050
  drawings = dedupeDrawingIds(nextDrawings.map((drawing) => normalizeDrawingState(drawing)));
@@ -8963,6 +9114,7 @@ function createChart(element, options = {}) {
8963
9114
  drawings: getDrawings(),
8964
9115
  indicators: getIndicators(),
8965
9116
  magnetMode: getMagnetMode(),
9117
+ priceScale: getPriceScale(),
8966
9118
  drawingDefaults
8967
9119
  };
8968
9120
  };
@@ -8982,6 +9134,9 @@ function createChart(element, options = {}) {
8982
9134
  if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
8983
9135
  setMagnetMode(state.magnetMode);
8984
9136
  }
9137
+ if (state.priceScale && typeof state.priceScale === "object") {
9138
+ setPriceScale(state.priceScale);
9139
+ }
8985
9140
  if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
8986
9141
  for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
8987
9142
  if (defaults && typeof defaults === "object") {
@@ -9076,6 +9231,8 @@ function createChart(element, options = {}) {
9076
9231
  onDrawingEditText,
9077
9232
  setMagnetMode,
9078
9233
  getMagnetMode,
9234
+ setPriceScale,
9235
+ getPriceScale,
9079
9236
  cancelDrawing,
9080
9237
  setTradeMarkers,
9081
9238
  setSelectedDrawing,
@@ -9101,7 +9258,9 @@ function createChart(element, options = {}) {
9101
9258
  export {
9102
9259
  FIB_DEFAULT_PALETTE,
9103
9260
  POSITION_DEFAULT_COLORS,
9261
+ SCRIPT_SOURCE_INPUT_VALUES,
9104
9262
  compileScriptIndicator,
9105
9263
  createChart,
9264
+ extractScriptInputs,
9106
9265
  validateScriptSource
9107
9266
  };
package/docs/API.md CHANGED
@@ -563,6 +563,7 @@ an up measurement, red for down.
563
563
  - `cancelDrawing(): boolean` — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns `true` if a draft was cancelled. Wire it to Escape.
564
564
  - Hold **Shift** while drawing or dragging an endpoint of a `trendline`/`ray`/`arrow` to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
565
565
  - `setMagnetMode(mode): void` / `getMagnetMode()` — magnet/snap for all drawing tools. `"none"` off, `"weak"` snaps to a candle's OHLC only when the cursor is near a value, `"strong"` always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
566
+ - `setPriceScale(options: { mode?: "linear" | "log" | "percent"; inverted?: boolean }): void` / `getPriceScale()` — price-scale modes for the main pane (TradingView-style). `"log"` maps prices through log10 (wide ranges get 1/2/5-per-decade axis marks). `"percent"` keeps the linear mapping but relabels the axis (ticks, last-price/PDC/H/L/bid/ask tags and the crosshair) as % change from the first visible bar's close, updating as you pan. `inverted: true` flips the axis top-to-bottom. All viewport state stays in plain price space, so switching modes never moves your zoom, and drawings/orders/indicators follow automatically. Indicator panes keep their own scales. Included in `saveState()` as `priceScale`.
566
567
  - `setActiveDrawingTool(tool: DrawingToolType | null): void` (`DrawingToolType` = `"horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note" | "parallel-channel" | "ellipse" | "arrow" | "brush" | "callout" | "measure"`)
567
568
  - `getActiveDrawingTool(): DrawingToolType | null`
568
569
  - `onActiveDrawingToolChange(handler: ((tool: DrawingToolType | null) => void) | null): void` — fires whenever the active tool changes, including the chart auto-reverting to the cursor after a tool completes (all tools do this when their shape is placed; `measure` does it after every measurement). Keeps host toolbars in sync without polling.
@@ -583,7 +584,7 @@ an up measurement, red for down.
583
584
  - `updateIndicator(id: string, patch: Partial<IndicatorInstanceOptions>): void`
584
585
  - `removeIndicator(id: string): void`
585
586
  - `setIndicators(indicators: IndicatorInstanceOptions[]): void`
586
- - `saveState(): ChartSavedState` — one JSON-serializable snapshot of the full user-visible setup: `{ version: 1, chartType, viewport, drawings, indicators, magnetMode, drawingDefaults }`. Chart *data* is not included; keep feeding bars through `setData`/`upsertBar`.
587
+ - `saveState(): ChartSavedState` — one JSON-serializable snapshot of the full user-visible setup: `{ version: 1, chartType, viewport, drawings, indicators, magnetMode, priceScale, drawingDefaults }`. Chart *data* is not included; keep feeding bars through `setData`/`upsertBar`.
587
588
  - `loadState(state: Partial<ChartSavedState>): void` — restore a `saveState()` blob. Tolerant of partial blobs (missing sections are left untouched), so it doubles as a bulk setter. Register custom script indicator plugins with `registerIndicator` *before* calling it so their instances resolve. Viewport is applied last so pane changes don't clobber the restore.
588
589
  - `takeScreenshot(options?: { type?: "image/png" | "image/jpeg"; quality?: number }): string` — returns a data URL of the current frame (default PNG) at the chart's device pixel ratio. Everything on the canvas is included: series, indicators, panes, drawings, legends.
589
590
 
@@ -653,6 +654,43 @@ Script contract:
653
654
  - `bars`: `Array<{ time: Date, o, h, l, c, v? }>` — the full series.
654
655
  - `inputs`: resolved input values (defaults merged with per-instance overrides). `showValueLine`
655
656
  is injected automatically and honored by separate panes.
657
+
658
+ ### Pine-style input declarations
659
+
660
+ Instead of maintaining a separate JSON `inputs` schema, scripts can declare their settings inline
661
+ at the top level — the same idea as TradingView's `input.int(14, "Length")`. The declared
662
+ constants always hold the live settings values, and hosts can auto-generate the settings dialog
663
+ from the declarations:
664
+
665
+ ```ts
666
+ const length = input.int("Length", 14, { min: 1, max: 500 });
667
+ const source = input.source("Source", "close"); // open/high/low/close/hl2/hlc3/ohlc4
668
+ const smooth = input.bool("Smooth", true);
669
+ const color = input.color("Line color", "#f59e0b");
670
+
671
+ function compute(bars, inputs, hp) {
672
+ const values = bars.map(b => source === "open" ? b.o : b.c);
673
+ const line = smooth ? hp.ema(values, length) : hp.sma(values, length);
674
+ return { plots: [{ title: "MA", values: line, color }] };
675
+ }
676
+ ```
677
+
678
+ Available declarations (all return the resolved value; `opts` is `{ key?, min?, max?, step? }`):
679
+
680
+ - `input.int(label, default, opts?)` / `input.float(label, default, opts?)` → number
681
+ - `input.bool(label, default?)` → boolean
682
+ - `input.color(label, default)` → color string
683
+ - `input.string(label, default?)` → string
684
+ - `input.select(label, default, choices)` → string (choices: `string[]` or `{value, label}[]`)
685
+ - `input.source(label?, default?)` → one of `open/high/low/close/hl2/hlc3/ohlc4`
686
+
687
+ Settings keys default to a slug of the label (`"Line color"` → `line_color`); pass
688
+ `{ key: "..." }` to pin one explicitly (keeps saved settings stable if you rename the label).
689
+
690
+ `extractScriptInputs(source: string): { inputs: ScriptIndicatorInputDef[], error: string | null }`
691
+ — runs the script's top level with a recording helper and returns the declared schema; hosts use
692
+ this to auto-generate settings dialogs. The legacy JSON `inputs` array on
693
+ `ScriptIndicatorDefinition` still works; on key collisions the inline declarations win.
656
694
  - `hp`: TA helpers, all `(values: Array<number|null>, length) => Array<number|null>` unless noted:
657
695
  `sma`, `ema`, `rma`, `highest`, `lowest`, `stdev`, `change(values, length = 1)`,
658
696
  `atr(bars, length)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.136",
3
+ "version": "0.1.137",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",