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/hyperprop-charting-library.cjs +203 -42
- package/dist/hyperprop-charting-library.d.ts +56 -1
- package/dist/hyperprop-charting-library.js +201 -42
- package/dist/index.cjs +203 -42
- package/dist/index.d.cts +56 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +201 -42
- package/docs/API.md +39 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
2484
|
+
return makeFactory(injectLoopGuards(source, "__hpGuard"));
|
|
2409
2485
|
} catch {
|
|
2410
|
-
|
|
2486
|
+
return makeFactory(source);
|
|
2411
2487
|
}
|
|
2412
|
-
guard.reset();
|
|
2413
|
-
return factory(guard);
|
|
2414
2488
|
};
|
|
2415
|
-
var
|
|
2489
|
+
var extractScriptInputs = (source) => {
|
|
2490
|
+
const registry = [];
|
|
2416
2491
|
try {
|
|
2417
|
-
|
|
2418
|
-
|
|
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
|
|
2518
|
+
let factory = null;
|
|
2440
2519
|
let compileError = null;
|
|
2441
2520
|
const guard = createScriptGuard();
|
|
2442
2521
|
try {
|
|
2443
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
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
|
-
}
|
|
5206
|
-
|
|
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(
|
|
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:
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
6383
|
-
const
|
|
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(
|
|
6810
|
-
|
|
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
|
|
6883
|
-
|
|
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)`.
|