hyperprop-charting-library 0.1.70 → 0.1.71

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.
@@ -2113,6 +2113,7 @@ function createChart(element, options = {}) {
2113
2113
  };
2114
2114
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2115
2115
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
2116
+ const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
2116
2117
  const hexToRgba = (hex, alpha) => {
2117
2118
  const cleaned = hex.replace("#", "");
2118
2119
  if (cleaned.length !== 6) {
@@ -2313,6 +2314,83 @@ function createChart(element, options = {}) {
2313
2314
  });
2314
2315
  ctx.font = prevFont;
2315
2316
  }
2317
+ } else if (drawing.type === "fib-extension") {
2318
+ const p0 = drawing.points[0];
2319
+ const p1 = drawing.points[1];
2320
+ const p2 = drawing.points[2];
2321
+ if (p0 && p1) {
2322
+ const x0 = xFromDrawingPoint(p0);
2323
+ const y0 = yFromPrice(p0.price);
2324
+ const x1 = xFromDrawingPoint(p1);
2325
+ const y1 = yFromPrice(p1.price);
2326
+ ctx.save();
2327
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
2328
+ ctx.setLineDash([4, 4]);
2329
+ ctx.lineWidth = 1;
2330
+ ctx.beginPath();
2331
+ ctx.moveTo(x0, y0);
2332
+ ctx.lineTo(x1, y1);
2333
+ if (p2) {
2334
+ ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
2335
+ }
2336
+ ctx.stroke();
2337
+ ctx.restore();
2338
+ drawDrawingHandle(x0, y0, drawing.color);
2339
+ drawDrawingHandle(x1, y1, drawing.color);
2340
+ if (p2) {
2341
+ const x2 = xFromDrawingPoint(p2);
2342
+ const y2 = yFromPrice(p2.price);
2343
+ drawDrawingHandle(x2, y2, drawing.color);
2344
+ const palette = drawing.colors.length > 0 ? drawing.colors : null;
2345
+ const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
2346
+ const move = p1.price - p0.price;
2347
+ const levelLines = FIB_EXT_RATIOS.map((ratio) => {
2348
+ const price = p2.price + move * ratio;
2349
+ return { ratio, price, y: yFromPrice(price) };
2350
+ });
2351
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2352
+ const top = levelLines[index];
2353
+ const bottom = levelLines[index + 1];
2354
+ const bandTop = Math.min(top.y, bottom.y);
2355
+ const bandBottom = Math.max(top.y, bottom.y);
2356
+ ctx.save();
2357
+ ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
2358
+ ctx.globalAlpha = draft ? 0.5 : 1;
2359
+ ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
2360
+ ctx.restore();
2361
+ }
2362
+ ctx.save();
2363
+ ctx.lineWidth = drawing.width;
2364
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2365
+ levelLines.forEach((level, index) => {
2366
+ ctx.strokeStyle = levelColorAt(index);
2367
+ ctx.beginPath();
2368
+ ctx.moveTo(crisp(chartLeft), crisp(level.y));
2369
+ ctx.lineTo(crisp(chartRight), crisp(level.y));
2370
+ ctx.stroke();
2371
+ });
2372
+ ctx.restore();
2373
+ const prevFont = ctx.font;
2374
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2375
+ levelLines.forEach((level, index) => {
2376
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2377
+ const textWidth = ctx.measureText(labelText).width;
2378
+ const padding = 4;
2379
+ const bgX = chartLeft + 4;
2380
+ const bgY = level.y - 9;
2381
+ ctx.fillStyle = mergedOptions.backgroundColor;
2382
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2383
+ ctx.fillStyle = levelColorAt(index);
2384
+ ctx.textAlign = "left";
2385
+ ctx.textBaseline = "middle";
2386
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2387
+ });
2388
+ ctx.font = prevFont;
2389
+ if (drawing.label) {
2390
+ drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
2391
+ }
2392
+ }
2393
+ }
2316
2394
  }
2317
2395
  ctx.restore();
2318
2396
  };
@@ -3474,6 +3552,41 @@ function createChart(element, options = {}) {
3474
3552
  return { drawing, target: "line" };
3475
3553
  }
3476
3554
  }
3555
+ } else if (drawing.type === "fib-extension") {
3556
+ const p0 = drawing.points[0];
3557
+ const p1 = drawing.points[1];
3558
+ const p2 = drawing.points[2];
3559
+ if (!p0 || !p1) continue;
3560
+ const x0 = canvasXFromDrawingPoint(p0);
3561
+ const y0 = canvasYFromDrawingPrice(p0.price);
3562
+ const x1 = canvasXFromDrawingPoint(p1);
3563
+ const y1 = canvasYFromDrawingPrice(p1.price);
3564
+ if (Math.hypot(x - x0, y - y0) <= 8) {
3565
+ return { drawing, target: "handle", pointIndex: 0 };
3566
+ }
3567
+ if (Math.hypot(x - x1, y - y1) <= 8) {
3568
+ return { drawing, target: "handle", pointIndex: 1 };
3569
+ }
3570
+ if (p2) {
3571
+ const x2 = canvasXFromDrawingPoint(p2);
3572
+ const y2 = canvasYFromDrawingPrice(p2.price);
3573
+ if (Math.hypot(x - x2, y - y2) <= 8) {
3574
+ return { drawing, target: "handle", pointIndex: 2 };
3575
+ }
3576
+ const move = p1.price - p0.price;
3577
+ const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
3578
+ for (const ratio of fibExtRatios) {
3579
+ const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
3580
+ if (Math.abs(y - lineY) <= 5) {
3581
+ return { drawing, target: "line" };
3582
+ }
3583
+ }
3584
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3585
+ return { drawing, target: "line" };
3586
+ }
3587
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3588
+ return { drawing, target: "line" };
3589
+ }
3477
3590
  }
3478
3591
  }
3479
3592
  return null;
@@ -3641,6 +3754,39 @@ function createChart(element, options = {}) {
3641
3754
  draw();
3642
3755
  return true;
3643
3756
  }
3757
+ if (activeDrawingTool === "fib-extension") {
3758
+ if (draftDrawing?.type === "fib-extension") {
3759
+ if (draftDrawing.points.length < 3) {
3760
+ draftDrawing = normalizeDrawingState({
3761
+ ...serializeDrawing(draftDrawing),
3762
+ points: [...draftDrawing.points.slice(0, -1), point, point]
3763
+ });
3764
+ draw();
3765
+ return true;
3766
+ }
3767
+ const completed = normalizeDrawingState({
3768
+ ...serializeDrawing(draftDrawing),
3769
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
3770
+ });
3771
+ drawings.push(completed);
3772
+ draftDrawing = null;
3773
+ activeDrawingTool = null;
3774
+ emitDrawingsChange();
3775
+ draw();
3776
+ return true;
3777
+ }
3778
+ const defaults = getDrawingToolDefaults("fib-extension");
3779
+ draftDrawing = normalizeDrawingState({
3780
+ type: "fib-extension",
3781
+ points: [point, point],
3782
+ color: defaults.color ?? "#2563eb",
3783
+ colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
3784
+ style: defaults.style ?? "solid",
3785
+ width: defaults.width ?? 1
3786
+ });
3787
+ draw();
3788
+ return true;
3789
+ }
3644
3790
  return false;
3645
3791
  };
3646
3792
  const setDrawingDefaults = (tool, defaults) => {
@@ -3913,12 +4059,12 @@ function createChart(element, options = {}) {
3913
4059
  setCrosshairPoint(null);
3914
4060
  return;
3915
4061
  }
3916
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
4062
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
3917
4063
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3918
4064
  if (nextPoint) {
3919
4065
  draftDrawing = {
3920
4066
  ...draftDrawing,
3921
- points: [draftDrawing.points[0], nextPoint]
4067
+ points: [...draftDrawing.points.slice(0, -1), nextPoint]
3922
4068
  };
3923
4069
  canvas.style.cursor = "crosshair";
3924
4070
  draw();
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
@@ -2088,6 +2088,7 @@ function createChart(element, options = {}) {
2088
2088
  };
2089
2089
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2090
2090
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
2091
+ const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
2091
2092
  const hexToRgba = (hex, alpha) => {
2092
2093
  const cleaned = hex.replace("#", "");
2093
2094
  if (cleaned.length !== 6) {
@@ -2288,6 +2289,83 @@ function createChart(element, options = {}) {
2288
2289
  });
2289
2290
  ctx.font = prevFont;
2290
2291
  }
2292
+ } else if (drawing.type === "fib-extension") {
2293
+ const p0 = drawing.points[0];
2294
+ const p1 = drawing.points[1];
2295
+ const p2 = drawing.points[2];
2296
+ if (p0 && p1) {
2297
+ const x0 = xFromDrawingPoint(p0);
2298
+ const y0 = yFromPrice(p0.price);
2299
+ const x1 = xFromDrawingPoint(p1);
2300
+ const y1 = yFromPrice(p1.price);
2301
+ ctx.save();
2302
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
2303
+ ctx.setLineDash([4, 4]);
2304
+ ctx.lineWidth = 1;
2305
+ ctx.beginPath();
2306
+ ctx.moveTo(x0, y0);
2307
+ ctx.lineTo(x1, y1);
2308
+ if (p2) {
2309
+ ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
2310
+ }
2311
+ ctx.stroke();
2312
+ ctx.restore();
2313
+ drawDrawingHandle(x0, y0, drawing.color);
2314
+ drawDrawingHandle(x1, y1, drawing.color);
2315
+ if (p2) {
2316
+ const x2 = xFromDrawingPoint(p2);
2317
+ const y2 = yFromPrice(p2.price);
2318
+ drawDrawingHandle(x2, y2, drawing.color);
2319
+ const palette = drawing.colors.length > 0 ? drawing.colors : null;
2320
+ const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
2321
+ const move = p1.price - p0.price;
2322
+ const levelLines = FIB_EXT_RATIOS.map((ratio) => {
2323
+ const price = p2.price + move * ratio;
2324
+ return { ratio, price, y: yFromPrice(price) };
2325
+ });
2326
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2327
+ const top = levelLines[index];
2328
+ const bottom = levelLines[index + 1];
2329
+ const bandTop = Math.min(top.y, bottom.y);
2330
+ const bandBottom = Math.max(top.y, bottom.y);
2331
+ ctx.save();
2332
+ ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
2333
+ ctx.globalAlpha = draft ? 0.5 : 1;
2334
+ ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
2335
+ ctx.restore();
2336
+ }
2337
+ ctx.save();
2338
+ ctx.lineWidth = drawing.width;
2339
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2340
+ levelLines.forEach((level, index) => {
2341
+ ctx.strokeStyle = levelColorAt(index);
2342
+ ctx.beginPath();
2343
+ ctx.moveTo(crisp(chartLeft), crisp(level.y));
2344
+ ctx.lineTo(crisp(chartRight), crisp(level.y));
2345
+ ctx.stroke();
2346
+ });
2347
+ ctx.restore();
2348
+ const prevFont = ctx.font;
2349
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2350
+ levelLines.forEach((level, index) => {
2351
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2352
+ const textWidth = ctx.measureText(labelText).width;
2353
+ const padding = 4;
2354
+ const bgX = chartLeft + 4;
2355
+ const bgY = level.y - 9;
2356
+ ctx.fillStyle = mergedOptions.backgroundColor;
2357
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2358
+ ctx.fillStyle = levelColorAt(index);
2359
+ ctx.textAlign = "left";
2360
+ ctx.textBaseline = "middle";
2361
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2362
+ });
2363
+ ctx.font = prevFont;
2364
+ if (drawing.label) {
2365
+ drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
2366
+ }
2367
+ }
2368
+ }
2291
2369
  }
2292
2370
  ctx.restore();
2293
2371
  };
@@ -3449,6 +3527,41 @@ function createChart(element, options = {}) {
3449
3527
  return { drawing, target: "line" };
3450
3528
  }
3451
3529
  }
3530
+ } else if (drawing.type === "fib-extension") {
3531
+ const p0 = drawing.points[0];
3532
+ const p1 = drawing.points[1];
3533
+ const p2 = drawing.points[2];
3534
+ if (!p0 || !p1) continue;
3535
+ const x0 = canvasXFromDrawingPoint(p0);
3536
+ const y0 = canvasYFromDrawingPrice(p0.price);
3537
+ const x1 = canvasXFromDrawingPoint(p1);
3538
+ const y1 = canvasYFromDrawingPrice(p1.price);
3539
+ if (Math.hypot(x - x0, y - y0) <= 8) {
3540
+ return { drawing, target: "handle", pointIndex: 0 };
3541
+ }
3542
+ if (Math.hypot(x - x1, y - y1) <= 8) {
3543
+ return { drawing, target: "handle", pointIndex: 1 };
3544
+ }
3545
+ if (p2) {
3546
+ const x2 = canvasXFromDrawingPoint(p2);
3547
+ const y2 = canvasYFromDrawingPrice(p2.price);
3548
+ if (Math.hypot(x - x2, y - y2) <= 8) {
3549
+ return { drawing, target: "handle", pointIndex: 2 };
3550
+ }
3551
+ const move = p1.price - p0.price;
3552
+ const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
3553
+ for (const ratio of fibExtRatios) {
3554
+ const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
3555
+ if (Math.abs(y - lineY) <= 5) {
3556
+ return { drawing, target: "line" };
3557
+ }
3558
+ }
3559
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3560
+ return { drawing, target: "line" };
3561
+ }
3562
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3563
+ return { drawing, target: "line" };
3564
+ }
3452
3565
  }
3453
3566
  }
3454
3567
  return null;
@@ -3616,6 +3729,39 @@ function createChart(element, options = {}) {
3616
3729
  draw();
3617
3730
  return true;
3618
3731
  }
3732
+ if (activeDrawingTool === "fib-extension") {
3733
+ if (draftDrawing?.type === "fib-extension") {
3734
+ if (draftDrawing.points.length < 3) {
3735
+ draftDrawing = normalizeDrawingState({
3736
+ ...serializeDrawing(draftDrawing),
3737
+ points: [...draftDrawing.points.slice(0, -1), point, point]
3738
+ });
3739
+ draw();
3740
+ return true;
3741
+ }
3742
+ const completed = normalizeDrawingState({
3743
+ ...serializeDrawing(draftDrawing),
3744
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
3745
+ });
3746
+ drawings.push(completed);
3747
+ draftDrawing = null;
3748
+ activeDrawingTool = null;
3749
+ emitDrawingsChange();
3750
+ draw();
3751
+ return true;
3752
+ }
3753
+ const defaults = getDrawingToolDefaults("fib-extension");
3754
+ draftDrawing = normalizeDrawingState({
3755
+ type: "fib-extension",
3756
+ points: [point, point],
3757
+ color: defaults.color ?? "#2563eb",
3758
+ colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
3759
+ style: defaults.style ?? "solid",
3760
+ width: defaults.width ?? 1
3761
+ });
3762
+ draw();
3763
+ return true;
3764
+ }
3619
3765
  return false;
3620
3766
  };
3621
3767
  const setDrawingDefaults = (tool, defaults) => {
@@ -3888,12 +4034,12 @@ function createChart(element, options = {}) {
3888
4034
  setCrosshairPoint(null);
3889
4035
  return;
3890
4036
  }
3891
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
4037
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
3892
4038
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3893
4039
  if (nextPoint) {
3894
4040
  draftDrawing = {
3895
4041
  ...draftDrawing,
3896
- points: [draftDrawing.points[0], nextPoint]
4042
+ points: [...draftDrawing.points.slice(0, -1), nextPoint]
3897
4043
  };
3898
4044
  canvas.style.cursor = "crosshair";
3899
4045
  draw();
package/dist/index.cjs CHANGED
@@ -2113,6 +2113,7 @@ function createChart(element, options = {}) {
2113
2113
  };
2114
2114
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2115
2115
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
2116
+ const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
2116
2117
  const hexToRgba = (hex, alpha) => {
2117
2118
  const cleaned = hex.replace("#", "");
2118
2119
  if (cleaned.length !== 6) {
@@ -2313,6 +2314,83 @@ function createChart(element, options = {}) {
2313
2314
  });
2314
2315
  ctx.font = prevFont;
2315
2316
  }
2317
+ } else if (drawing.type === "fib-extension") {
2318
+ const p0 = drawing.points[0];
2319
+ const p1 = drawing.points[1];
2320
+ const p2 = drawing.points[2];
2321
+ if (p0 && p1) {
2322
+ const x0 = xFromDrawingPoint(p0);
2323
+ const y0 = yFromPrice(p0.price);
2324
+ const x1 = xFromDrawingPoint(p1);
2325
+ const y1 = yFromPrice(p1.price);
2326
+ ctx.save();
2327
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
2328
+ ctx.setLineDash([4, 4]);
2329
+ ctx.lineWidth = 1;
2330
+ ctx.beginPath();
2331
+ ctx.moveTo(x0, y0);
2332
+ ctx.lineTo(x1, y1);
2333
+ if (p2) {
2334
+ ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
2335
+ }
2336
+ ctx.stroke();
2337
+ ctx.restore();
2338
+ drawDrawingHandle(x0, y0, drawing.color);
2339
+ drawDrawingHandle(x1, y1, drawing.color);
2340
+ if (p2) {
2341
+ const x2 = xFromDrawingPoint(p2);
2342
+ const y2 = yFromPrice(p2.price);
2343
+ drawDrawingHandle(x2, y2, drawing.color);
2344
+ const palette = drawing.colors.length > 0 ? drawing.colors : null;
2345
+ const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
2346
+ const move = p1.price - p0.price;
2347
+ const levelLines = FIB_EXT_RATIOS.map((ratio) => {
2348
+ const price = p2.price + move * ratio;
2349
+ return { ratio, price, y: yFromPrice(price) };
2350
+ });
2351
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2352
+ const top = levelLines[index];
2353
+ const bottom = levelLines[index + 1];
2354
+ const bandTop = Math.min(top.y, bottom.y);
2355
+ const bandBottom = Math.max(top.y, bottom.y);
2356
+ ctx.save();
2357
+ ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
2358
+ ctx.globalAlpha = draft ? 0.5 : 1;
2359
+ ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
2360
+ ctx.restore();
2361
+ }
2362
+ ctx.save();
2363
+ ctx.lineWidth = drawing.width;
2364
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2365
+ levelLines.forEach((level, index) => {
2366
+ ctx.strokeStyle = levelColorAt(index);
2367
+ ctx.beginPath();
2368
+ ctx.moveTo(crisp(chartLeft), crisp(level.y));
2369
+ ctx.lineTo(crisp(chartRight), crisp(level.y));
2370
+ ctx.stroke();
2371
+ });
2372
+ ctx.restore();
2373
+ const prevFont = ctx.font;
2374
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2375
+ levelLines.forEach((level, index) => {
2376
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2377
+ const textWidth = ctx.measureText(labelText).width;
2378
+ const padding = 4;
2379
+ const bgX = chartLeft + 4;
2380
+ const bgY = level.y - 9;
2381
+ ctx.fillStyle = mergedOptions.backgroundColor;
2382
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2383
+ ctx.fillStyle = levelColorAt(index);
2384
+ ctx.textAlign = "left";
2385
+ ctx.textBaseline = "middle";
2386
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2387
+ });
2388
+ ctx.font = prevFont;
2389
+ if (drawing.label) {
2390
+ drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
2391
+ }
2392
+ }
2393
+ }
2316
2394
  }
2317
2395
  ctx.restore();
2318
2396
  };
@@ -3474,6 +3552,41 @@ function createChart(element, options = {}) {
3474
3552
  return { drawing, target: "line" };
3475
3553
  }
3476
3554
  }
3555
+ } else if (drawing.type === "fib-extension") {
3556
+ const p0 = drawing.points[0];
3557
+ const p1 = drawing.points[1];
3558
+ const p2 = drawing.points[2];
3559
+ if (!p0 || !p1) continue;
3560
+ const x0 = canvasXFromDrawingPoint(p0);
3561
+ const y0 = canvasYFromDrawingPrice(p0.price);
3562
+ const x1 = canvasXFromDrawingPoint(p1);
3563
+ const y1 = canvasYFromDrawingPrice(p1.price);
3564
+ if (Math.hypot(x - x0, y - y0) <= 8) {
3565
+ return { drawing, target: "handle", pointIndex: 0 };
3566
+ }
3567
+ if (Math.hypot(x - x1, y - y1) <= 8) {
3568
+ return { drawing, target: "handle", pointIndex: 1 };
3569
+ }
3570
+ if (p2) {
3571
+ const x2 = canvasXFromDrawingPoint(p2);
3572
+ const y2 = canvasYFromDrawingPrice(p2.price);
3573
+ if (Math.hypot(x - x2, y - y2) <= 8) {
3574
+ return { drawing, target: "handle", pointIndex: 2 };
3575
+ }
3576
+ const move = p1.price - p0.price;
3577
+ const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
3578
+ for (const ratio of fibExtRatios) {
3579
+ const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
3580
+ if (Math.abs(y - lineY) <= 5) {
3581
+ return { drawing, target: "line" };
3582
+ }
3583
+ }
3584
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3585
+ return { drawing, target: "line" };
3586
+ }
3587
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3588
+ return { drawing, target: "line" };
3589
+ }
3477
3590
  }
3478
3591
  }
3479
3592
  return null;
@@ -3641,6 +3754,39 @@ function createChart(element, options = {}) {
3641
3754
  draw();
3642
3755
  return true;
3643
3756
  }
3757
+ if (activeDrawingTool === "fib-extension") {
3758
+ if (draftDrawing?.type === "fib-extension") {
3759
+ if (draftDrawing.points.length < 3) {
3760
+ draftDrawing = normalizeDrawingState({
3761
+ ...serializeDrawing(draftDrawing),
3762
+ points: [...draftDrawing.points.slice(0, -1), point, point]
3763
+ });
3764
+ draw();
3765
+ return true;
3766
+ }
3767
+ const completed = normalizeDrawingState({
3768
+ ...serializeDrawing(draftDrawing),
3769
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
3770
+ });
3771
+ drawings.push(completed);
3772
+ draftDrawing = null;
3773
+ activeDrawingTool = null;
3774
+ emitDrawingsChange();
3775
+ draw();
3776
+ return true;
3777
+ }
3778
+ const defaults = getDrawingToolDefaults("fib-extension");
3779
+ draftDrawing = normalizeDrawingState({
3780
+ type: "fib-extension",
3781
+ points: [point, point],
3782
+ color: defaults.color ?? "#2563eb",
3783
+ colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
3784
+ style: defaults.style ?? "solid",
3785
+ width: defaults.width ?? 1
3786
+ });
3787
+ draw();
3788
+ return true;
3789
+ }
3644
3790
  return false;
3645
3791
  };
3646
3792
  const setDrawingDefaults = (tool, defaults) => {
@@ -3913,12 +4059,12 @@ function createChart(element, options = {}) {
3913
4059
  setCrosshairPoint(null);
3914
4060
  return;
3915
4061
  }
3916
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
4062
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
3917
4063
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3918
4064
  if (nextPoint) {
3919
4065
  draftDrawing = {
3920
4066
  ...draftDrawing,
3921
- points: [draftDrawing.points[0], nextPoint]
4067
+ points: [...draftDrawing.points.slice(0, -1), nextPoint]
3922
4068
  };
3923
4069
  canvas.style.cursor = "crosshair";
3924
4070
  draw();
package/dist/index.d.cts CHANGED
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.d.ts CHANGED
@@ -44,7 +44,7 @@ interface ChartOptions {
44
44
  drawings?: DrawingObjectOptions[];
45
45
  }
46
46
  type IndicatorPane = "overlay" | "separate";
47
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.js CHANGED
@@ -2088,6 +2088,7 @@ function createChart(element, options = {}) {
2088
2088
  };
2089
2089
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2090
2090
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
2091
+ const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
2091
2092
  const hexToRgba = (hex, alpha) => {
2092
2093
  const cleaned = hex.replace("#", "");
2093
2094
  if (cleaned.length !== 6) {
@@ -2288,6 +2289,83 @@ function createChart(element, options = {}) {
2288
2289
  });
2289
2290
  ctx.font = prevFont;
2290
2291
  }
2292
+ } else if (drawing.type === "fib-extension") {
2293
+ const p0 = drawing.points[0];
2294
+ const p1 = drawing.points[1];
2295
+ const p2 = drawing.points[2];
2296
+ if (p0 && p1) {
2297
+ const x0 = xFromDrawingPoint(p0);
2298
+ const y0 = yFromPrice(p0.price);
2299
+ const x1 = xFromDrawingPoint(p1);
2300
+ const y1 = yFromPrice(p1.price);
2301
+ ctx.save();
2302
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
2303
+ ctx.setLineDash([4, 4]);
2304
+ ctx.lineWidth = 1;
2305
+ ctx.beginPath();
2306
+ ctx.moveTo(x0, y0);
2307
+ ctx.lineTo(x1, y1);
2308
+ if (p2) {
2309
+ ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
2310
+ }
2311
+ ctx.stroke();
2312
+ ctx.restore();
2313
+ drawDrawingHandle(x0, y0, drawing.color);
2314
+ drawDrawingHandle(x1, y1, drawing.color);
2315
+ if (p2) {
2316
+ const x2 = xFromDrawingPoint(p2);
2317
+ const y2 = yFromPrice(p2.price);
2318
+ drawDrawingHandle(x2, y2, drawing.color);
2319
+ const palette = drawing.colors.length > 0 ? drawing.colors : null;
2320
+ const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
2321
+ const move = p1.price - p0.price;
2322
+ const levelLines = FIB_EXT_RATIOS.map((ratio) => {
2323
+ const price = p2.price + move * ratio;
2324
+ return { ratio, price, y: yFromPrice(price) };
2325
+ });
2326
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2327
+ const top = levelLines[index];
2328
+ const bottom = levelLines[index + 1];
2329
+ const bandTop = Math.min(top.y, bottom.y);
2330
+ const bandBottom = Math.max(top.y, bottom.y);
2331
+ ctx.save();
2332
+ ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
2333
+ ctx.globalAlpha = draft ? 0.5 : 1;
2334
+ ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
2335
+ ctx.restore();
2336
+ }
2337
+ ctx.save();
2338
+ ctx.lineWidth = drawing.width;
2339
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2340
+ levelLines.forEach((level, index) => {
2341
+ ctx.strokeStyle = levelColorAt(index);
2342
+ ctx.beginPath();
2343
+ ctx.moveTo(crisp(chartLeft), crisp(level.y));
2344
+ ctx.lineTo(crisp(chartRight), crisp(level.y));
2345
+ ctx.stroke();
2346
+ });
2347
+ ctx.restore();
2348
+ const prevFont = ctx.font;
2349
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2350
+ levelLines.forEach((level, index) => {
2351
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2352
+ const textWidth = ctx.measureText(labelText).width;
2353
+ const padding = 4;
2354
+ const bgX = chartLeft + 4;
2355
+ const bgY = level.y - 9;
2356
+ ctx.fillStyle = mergedOptions.backgroundColor;
2357
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2358
+ ctx.fillStyle = levelColorAt(index);
2359
+ ctx.textAlign = "left";
2360
+ ctx.textBaseline = "middle";
2361
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2362
+ });
2363
+ ctx.font = prevFont;
2364
+ if (drawing.label) {
2365
+ drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
2366
+ }
2367
+ }
2368
+ }
2291
2369
  }
2292
2370
  ctx.restore();
2293
2371
  };
@@ -3449,6 +3527,41 @@ function createChart(element, options = {}) {
3449
3527
  return { drawing, target: "line" };
3450
3528
  }
3451
3529
  }
3530
+ } else if (drawing.type === "fib-extension") {
3531
+ const p0 = drawing.points[0];
3532
+ const p1 = drawing.points[1];
3533
+ const p2 = drawing.points[2];
3534
+ if (!p0 || !p1) continue;
3535
+ const x0 = canvasXFromDrawingPoint(p0);
3536
+ const y0 = canvasYFromDrawingPrice(p0.price);
3537
+ const x1 = canvasXFromDrawingPoint(p1);
3538
+ const y1 = canvasYFromDrawingPrice(p1.price);
3539
+ if (Math.hypot(x - x0, y - y0) <= 8) {
3540
+ return { drawing, target: "handle", pointIndex: 0 };
3541
+ }
3542
+ if (Math.hypot(x - x1, y - y1) <= 8) {
3543
+ return { drawing, target: "handle", pointIndex: 1 };
3544
+ }
3545
+ if (p2) {
3546
+ const x2 = canvasXFromDrawingPoint(p2);
3547
+ const y2 = canvasYFromDrawingPrice(p2.price);
3548
+ if (Math.hypot(x - x2, y - y2) <= 8) {
3549
+ return { drawing, target: "handle", pointIndex: 2 };
3550
+ }
3551
+ const move = p1.price - p0.price;
3552
+ const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
3553
+ for (const ratio of fibExtRatios) {
3554
+ const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
3555
+ if (Math.abs(y - lineY) <= 5) {
3556
+ return { drawing, target: "line" };
3557
+ }
3558
+ }
3559
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3560
+ return { drawing, target: "line" };
3561
+ }
3562
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3563
+ return { drawing, target: "line" };
3564
+ }
3452
3565
  }
3453
3566
  }
3454
3567
  return null;
@@ -3616,6 +3729,39 @@ function createChart(element, options = {}) {
3616
3729
  draw();
3617
3730
  return true;
3618
3731
  }
3732
+ if (activeDrawingTool === "fib-extension") {
3733
+ if (draftDrawing?.type === "fib-extension") {
3734
+ if (draftDrawing.points.length < 3) {
3735
+ draftDrawing = normalizeDrawingState({
3736
+ ...serializeDrawing(draftDrawing),
3737
+ points: [...draftDrawing.points.slice(0, -1), point, point]
3738
+ });
3739
+ draw();
3740
+ return true;
3741
+ }
3742
+ const completed = normalizeDrawingState({
3743
+ ...serializeDrawing(draftDrawing),
3744
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
3745
+ });
3746
+ drawings.push(completed);
3747
+ draftDrawing = null;
3748
+ activeDrawingTool = null;
3749
+ emitDrawingsChange();
3750
+ draw();
3751
+ return true;
3752
+ }
3753
+ const defaults = getDrawingToolDefaults("fib-extension");
3754
+ draftDrawing = normalizeDrawingState({
3755
+ type: "fib-extension",
3756
+ points: [point, point],
3757
+ color: defaults.color ?? "#2563eb",
3758
+ colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
3759
+ style: defaults.style ?? "solid",
3760
+ width: defaults.width ?? 1
3761
+ });
3762
+ draw();
3763
+ return true;
3764
+ }
3619
3765
  return false;
3620
3766
  };
3621
3767
  const setDrawingDefaults = (tool, defaults) => {
@@ -3888,12 +4034,12 @@ function createChart(element, options = {}) {
3888
4034
  setCrosshairPoint(null);
3889
4035
  return;
3890
4036
  }
3891
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
4037
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
3892
4038
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3893
4039
  if (nextPoint) {
3894
4040
  draftDrawing = {
3895
4041
  ...draftDrawing,
3896
- points: [draftDrawing.points[0], nextPoint]
4042
+ points: [...draftDrawing.points.slice(0, -1), nextPoint]
3897
4043
  };
3898
4044
  canvas.style.cursor = "crosshair";
3899
4045
  draw();
package/docs/API.md CHANGED
@@ -416,11 +416,12 @@ Volume style inputs:
416
416
  Drawings are user-created chart tools, separate from indicators. They are interactive chart objects like horizontal lines and trendlines.
417
417
 
418
418
  - `id?: string`
419
- - `type: "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement"`
419
+ - `type: "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension"`
420
420
  - `horizontal-line` / `vertical-line`: single-point, full-width/full-height line (one click to place)
421
421
  - `trendline`: two-point segment (click start, click end)
422
422
  - `ray`: two-point line that extends infinitely past the second point
423
423
  - `fib-retracement`: two-point retracement with levels/bands
424
+ - `fib-extension`: trend-based three-point extension (click trend start, trend end, then the projection origin); levels project from the third point by the first→second move
424
425
  - `points: DrawingPoint[]`
425
426
  - `visible?: boolean`
426
427
  - `color?: string`
@@ -447,6 +448,7 @@ chart.setActiveDrawingTool("vertical-line"); // next plot click creates a vert
447
448
  chart.setActiveDrawingTool("trendline"); // first click starts, second click commits
448
449
  chart.setActiveDrawingTool("ray"); // first click starts, second click commits (extends past p2)
449
450
  chart.setActiveDrawingTool("fib-retracement"); // first click starts, second click commits
451
+ chart.setActiveDrawingTool("fib-extension"); // three clicks: trend start, trend end, projection origin
450
452
  chart.setActiveDrawingTool(null); // back to normal cursor/pan
451
453
  ```
452
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",