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