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