hyperprop-charting-library 0.1.58 → 0.1.60

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.
@@ -2021,6 +2021,16 @@ function createChart(element, options = {}) {
2021
2021
  return chartBottom - (price - yMin) / yRange * chartHeight;
2022
2022
  };
2023
2023
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2024
+ const FIB_LEVELS = [
2025
+ { ratio: 0, color: "#787b86" },
2026
+ { ratio: 0.236, color: "#f23645", band: "rgba(242,54,69,0.10)" },
2027
+ { ratio: 0.382, color: "#ff9800", band: "rgba(255,152,0,0.10)" },
2028
+ { ratio: 0.5, color: "#4caf50", band: "rgba(76,175,80,0.10)" },
2029
+ { ratio: 0.618, color: "#089981", band: "rgba(8,153,129,0.10)" },
2030
+ { ratio: 0.786, color: "#00bcd4", band: "rgba(0,188,212,0.10)" },
2031
+ { ratio: 1, color: "#787b86", band: "rgba(120,123,134,0.10)" },
2032
+ { ratio: 1.618, color: "#2962ff", band: "rgba(41,98,255,0.08)" }
2033
+ ];
2024
2034
  const drawDrawingHandle = (x, y, color) => {
2025
2035
  ctx.save();
2026
2036
  ctx.setLineDash([]);
@@ -2033,27 +2043,24 @@ function createChart(element, options = {}) {
2033
2043
  ctx.stroke();
2034
2044
  ctx.restore();
2035
2045
  };
2036
- const drawDrawingLabel = (labelText, anchorX, anchorY, color, angle) => {
2046
+ const drawDrawingLabel = (labelText, anchorX, anchorY, color) => {
2037
2047
  if (!labelText) return;
2038
2048
  ctx.save();
2039
- ctx.translate(anchorX, anchorY);
2040
- ctx.rotate(angle);
2041
- ctx.translate(0, -8);
2042
2049
  const prevFont = ctx.font;
2043
2050
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2044
2051
  const textWidth = ctx.measureText(labelText).width;
2045
2052
  const paddingX = 6;
2046
- const paddingY = 3;
2047
2053
  const bgWidth = textWidth + paddingX * 2;
2048
2054
  const bgHeight = 18;
2055
+ const bgX = anchorX - bgWidth / 2;
2056
+ const bgY = anchorY - bgHeight - 6;
2049
2057
  ctx.fillStyle = `${color}1f`;
2050
- fillRoundedRect(-bgWidth / 2, -bgHeight, bgWidth, bgHeight, 4);
2058
+ fillRoundedRect(bgX, bgY, bgWidth, bgHeight, 4);
2051
2059
  ctx.fillStyle = color;
2052
2060
  ctx.textAlign = "center";
2053
2061
  ctx.textBaseline = "middle";
2054
- ctx.fillText(labelText, 0, -bgHeight / 2);
2062
+ ctx.fillText(labelText, anchorX, bgY + bgHeight / 2);
2055
2063
  ctx.font = prevFont;
2056
- void paddingY;
2057
2064
  ctx.restore();
2058
2065
  };
2059
2066
  const drawDrawing = (drawing, draft = false) => {
@@ -2076,7 +2083,8 @@ function createChart(element, options = {}) {
2076
2083
  ctx.stroke();
2077
2084
  drawDrawingHandle(handleX, y, drawing.color);
2078
2085
  if (drawing.label) {
2079
- drawDrawingLabel(drawing.label, chartLeft + 60, y, drawing.color, 0);
2086
+ const midX = (chartLeft + chartRight) / 2;
2087
+ drawDrawingLabel(drawing.label, midX, y, drawing.color);
2080
2088
  }
2081
2089
  }
2082
2090
  } else if (drawing.type === "trendline") {
@@ -2096,9 +2104,74 @@ function createChart(element, options = {}) {
2096
2104
  if (drawing.label) {
2097
2105
  const midX = (firstX + secondX) / 2;
2098
2106
  const midY = (firstY + secondY) / 2;
2099
- const angle = Math.atan2(secondY - firstY, secondX - firstX);
2100
- drawDrawingLabel(drawing.label, midX, midY, drawing.color, angle);
2107
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
2108
+ }
2109
+ }
2110
+ } else if (drawing.type === "fib-retracement") {
2111
+ const first = drawing.points[0];
2112
+ const second = drawing.points[1];
2113
+ if (first && second) {
2114
+ const firstX = xFromDrawingPoint(first);
2115
+ const secondX = xFromDrawingPoint(second);
2116
+ const firstY = yFromPrice(first.price);
2117
+ const secondY = yFromPrice(second.price);
2118
+ const lineLeft = chartLeft;
2119
+ const lineRight = chartRight;
2120
+ const levelLines = FIB_LEVELS.map((level) => {
2121
+ const price = first.price + (second.price - first.price) * (1 - level.ratio);
2122
+ return { ...level, price, y: yFromPrice(price) };
2123
+ });
2124
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2125
+ const top = levelLines[index];
2126
+ const bottom = levelLines[index + 1];
2127
+ const fillColor = bottom.band ?? top.band;
2128
+ if (!fillColor) continue;
2129
+ const bandTop = Math.min(top.y, bottom.y);
2130
+ const bandBottom = Math.max(top.y, bottom.y);
2131
+ ctx.save();
2132
+ ctx.fillStyle = fillColor;
2133
+ ctx.globalAlpha = draft ? 0.5 : 1;
2134
+ ctx.fillRect(lineLeft, bandTop, lineRight - lineLeft, bandBottom - bandTop);
2135
+ ctx.restore();
2136
+ }
2137
+ ctx.save();
2138
+ ctx.lineWidth = drawing.width;
2139
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2140
+ for (const level of levelLines) {
2141
+ ctx.strokeStyle = level.color;
2142
+ ctx.beginPath();
2143
+ ctx.moveTo(crisp(lineLeft), crisp(level.y));
2144
+ ctx.lineTo(crisp(lineRight), crisp(level.y));
2145
+ ctx.stroke();
2101
2146
  }
2147
+ ctx.restore();
2148
+ ctx.save();
2149
+ ctx.strokeStyle = "rgba(148,163,184,0.7)";
2150
+ ctx.setLineDash([4, 4]);
2151
+ ctx.lineWidth = 1;
2152
+ ctx.beginPath();
2153
+ ctx.moveTo(firstX, firstY);
2154
+ ctx.lineTo(secondX, secondY);
2155
+ ctx.stroke();
2156
+ ctx.restore();
2157
+ drawDrawingHandle(firstX, firstY, drawing.color);
2158
+ drawDrawingHandle(secondX, secondY, drawing.color);
2159
+ const prevFont = ctx.font;
2160
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2161
+ for (const level of levelLines) {
2162
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2163
+ const textWidth = ctx.measureText(labelText).width;
2164
+ const padding = 4;
2165
+ const bgX = lineLeft + 4;
2166
+ const bgY = level.y - 9;
2167
+ ctx.fillStyle = mergedOptions.backgroundColor;
2168
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2169
+ ctx.fillStyle = level.color;
2170
+ ctx.textAlign = "left";
2171
+ ctx.textBaseline = "middle";
2172
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2173
+ }
2174
+ ctx.font = prevFont;
2102
2175
  }
2103
2176
  }
2104
2177
  ctx.restore();
@@ -3186,6 +3259,28 @@ function createChart(element, options = {}) {
3186
3259
  if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3187
3260
  return { drawing, target: "line" };
3188
3261
  }
3262
+ } else if (drawing.type === "fib-retracement") {
3263
+ const first = drawing.points[0];
3264
+ const second = drawing.points[1];
3265
+ if (!first || !second) continue;
3266
+ const x1 = canvasXFromDrawingPoint(first);
3267
+ const y1 = canvasYFromDrawingPrice(first.price);
3268
+ const x2 = canvasXFromDrawingPoint(second);
3269
+ const y2 = canvasYFromDrawingPrice(second.price);
3270
+ if (Math.hypot(x - x1, y - y1) <= 9) {
3271
+ return { drawing, target: "handle", pointIndex: 0 };
3272
+ }
3273
+ if (Math.hypot(x - x2, y - y2) <= 9) {
3274
+ return { drawing, target: "handle", pointIndex: 1 };
3275
+ }
3276
+ const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
3277
+ for (const ratio of fibRatios) {
3278
+ const price = first.price + (second.price - first.price) * (1 - ratio);
3279
+ const lineY = canvasYFromDrawingPrice(price);
3280
+ if (Math.abs(y - lineY) <= 5) {
3281
+ return { drawing, target: "line" };
3282
+ }
3283
+ }
3189
3284
  }
3190
3285
  }
3191
3286
  return null;
@@ -3287,6 +3382,29 @@ function createChart(element, options = {}) {
3287
3382
  draw();
3288
3383
  return true;
3289
3384
  }
3385
+ if (activeDrawingTool === "fib-retracement") {
3386
+ if (draftDrawing?.type === "fib-retracement") {
3387
+ const completed = normalizeDrawingState({
3388
+ ...serializeDrawing(draftDrawing),
3389
+ points: [draftDrawing.points[0], point]
3390
+ });
3391
+ drawings.push(completed);
3392
+ draftDrawing = null;
3393
+ activeDrawingTool = null;
3394
+ emitDrawingsChange();
3395
+ draw();
3396
+ return true;
3397
+ }
3398
+ draftDrawing = normalizeDrawingState({
3399
+ type: "fib-retracement",
3400
+ points: [point, point],
3401
+ color: "#2563eb",
3402
+ style: "solid",
3403
+ width: 1
3404
+ });
3405
+ draw();
3406
+ return true;
3407
+ }
3290
3408
  return false;
3291
3409
  };
3292
3410
  const updateDrawingDrag = (x, y) => {
@@ -3522,7 +3640,7 @@ function createChart(element, options = {}) {
3522
3640
  setCrosshairPoint(null);
3523
3641
  return;
3524
3642
  }
3525
- if (draftDrawing && activeDrawingTool === "trendline") {
3643
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "fib-retracement")) {
3526
3644
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3527
3645
  if (nextPoint) {
3528
3646
  draftDrawing = {
@@ -43,7 +43,7 @@ interface ChartOptions {
43
43
  drawings?: DrawingObjectOptions[];
44
44
  }
45
45
  type IndicatorPane = "overlay" | "separate";
46
- type DrawingToolType = "horizontal-line" | "trendline";
46
+ type DrawingToolType = "horizontal-line" | "trendline" | "fib-retracement";
47
47
  interface DrawingPoint {
48
48
  index: number;
49
49
  price: number;
@@ -1997,6 +1997,16 @@ function createChart(element, options = {}) {
1997
1997
  return chartBottom - (price - yMin) / yRange * chartHeight;
1998
1998
  };
1999
1999
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2000
+ const FIB_LEVELS = [
2001
+ { ratio: 0, color: "#787b86" },
2002
+ { ratio: 0.236, color: "#f23645", band: "rgba(242,54,69,0.10)" },
2003
+ { ratio: 0.382, color: "#ff9800", band: "rgba(255,152,0,0.10)" },
2004
+ { ratio: 0.5, color: "#4caf50", band: "rgba(76,175,80,0.10)" },
2005
+ { ratio: 0.618, color: "#089981", band: "rgba(8,153,129,0.10)" },
2006
+ { ratio: 0.786, color: "#00bcd4", band: "rgba(0,188,212,0.10)" },
2007
+ { ratio: 1, color: "#787b86", band: "rgba(120,123,134,0.10)" },
2008
+ { ratio: 1.618, color: "#2962ff", band: "rgba(41,98,255,0.08)" }
2009
+ ];
2000
2010
  const drawDrawingHandle = (x, y, color) => {
2001
2011
  ctx.save();
2002
2012
  ctx.setLineDash([]);
@@ -2009,27 +2019,24 @@ function createChart(element, options = {}) {
2009
2019
  ctx.stroke();
2010
2020
  ctx.restore();
2011
2021
  };
2012
- const drawDrawingLabel = (labelText, anchorX, anchorY, color, angle) => {
2022
+ const drawDrawingLabel = (labelText, anchorX, anchorY, color) => {
2013
2023
  if (!labelText) return;
2014
2024
  ctx.save();
2015
- ctx.translate(anchorX, anchorY);
2016
- ctx.rotate(angle);
2017
- ctx.translate(0, -8);
2018
2025
  const prevFont = ctx.font;
2019
2026
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2020
2027
  const textWidth = ctx.measureText(labelText).width;
2021
2028
  const paddingX = 6;
2022
- const paddingY = 3;
2023
2029
  const bgWidth = textWidth + paddingX * 2;
2024
2030
  const bgHeight = 18;
2031
+ const bgX = anchorX - bgWidth / 2;
2032
+ const bgY = anchorY - bgHeight - 6;
2025
2033
  ctx.fillStyle = `${color}1f`;
2026
- fillRoundedRect(-bgWidth / 2, -bgHeight, bgWidth, bgHeight, 4);
2034
+ fillRoundedRect(bgX, bgY, bgWidth, bgHeight, 4);
2027
2035
  ctx.fillStyle = color;
2028
2036
  ctx.textAlign = "center";
2029
2037
  ctx.textBaseline = "middle";
2030
- ctx.fillText(labelText, 0, -bgHeight / 2);
2038
+ ctx.fillText(labelText, anchorX, bgY + bgHeight / 2);
2031
2039
  ctx.font = prevFont;
2032
- void paddingY;
2033
2040
  ctx.restore();
2034
2041
  };
2035
2042
  const drawDrawing = (drawing, draft = false) => {
@@ -2052,7 +2059,8 @@ function createChart(element, options = {}) {
2052
2059
  ctx.stroke();
2053
2060
  drawDrawingHandle(handleX, y, drawing.color);
2054
2061
  if (drawing.label) {
2055
- drawDrawingLabel(drawing.label, chartLeft + 60, y, drawing.color, 0);
2062
+ const midX = (chartLeft + chartRight) / 2;
2063
+ drawDrawingLabel(drawing.label, midX, y, drawing.color);
2056
2064
  }
2057
2065
  }
2058
2066
  } else if (drawing.type === "trendline") {
@@ -2072,9 +2080,74 @@ function createChart(element, options = {}) {
2072
2080
  if (drawing.label) {
2073
2081
  const midX = (firstX + secondX) / 2;
2074
2082
  const midY = (firstY + secondY) / 2;
2075
- const angle = Math.atan2(secondY - firstY, secondX - firstX);
2076
- drawDrawingLabel(drawing.label, midX, midY, drawing.color, angle);
2083
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
2084
+ }
2085
+ }
2086
+ } else if (drawing.type === "fib-retracement") {
2087
+ const first = drawing.points[0];
2088
+ const second = drawing.points[1];
2089
+ if (first && second) {
2090
+ const firstX = xFromDrawingPoint(first);
2091
+ const secondX = xFromDrawingPoint(second);
2092
+ const firstY = yFromPrice(first.price);
2093
+ const secondY = yFromPrice(second.price);
2094
+ const lineLeft = chartLeft;
2095
+ const lineRight = chartRight;
2096
+ const levelLines = FIB_LEVELS.map((level) => {
2097
+ const price = first.price + (second.price - first.price) * (1 - level.ratio);
2098
+ return { ...level, price, y: yFromPrice(price) };
2099
+ });
2100
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2101
+ const top = levelLines[index];
2102
+ const bottom = levelLines[index + 1];
2103
+ const fillColor = bottom.band ?? top.band;
2104
+ if (!fillColor) continue;
2105
+ const bandTop = Math.min(top.y, bottom.y);
2106
+ const bandBottom = Math.max(top.y, bottom.y);
2107
+ ctx.save();
2108
+ ctx.fillStyle = fillColor;
2109
+ ctx.globalAlpha = draft ? 0.5 : 1;
2110
+ ctx.fillRect(lineLeft, bandTop, lineRight - lineLeft, bandBottom - bandTop);
2111
+ ctx.restore();
2112
+ }
2113
+ ctx.save();
2114
+ ctx.lineWidth = drawing.width;
2115
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2116
+ for (const level of levelLines) {
2117
+ ctx.strokeStyle = level.color;
2118
+ ctx.beginPath();
2119
+ ctx.moveTo(crisp(lineLeft), crisp(level.y));
2120
+ ctx.lineTo(crisp(lineRight), crisp(level.y));
2121
+ ctx.stroke();
2077
2122
  }
2123
+ ctx.restore();
2124
+ ctx.save();
2125
+ ctx.strokeStyle = "rgba(148,163,184,0.7)";
2126
+ ctx.setLineDash([4, 4]);
2127
+ ctx.lineWidth = 1;
2128
+ ctx.beginPath();
2129
+ ctx.moveTo(firstX, firstY);
2130
+ ctx.lineTo(secondX, secondY);
2131
+ ctx.stroke();
2132
+ ctx.restore();
2133
+ drawDrawingHandle(firstX, firstY, drawing.color);
2134
+ drawDrawingHandle(secondX, secondY, drawing.color);
2135
+ const prevFont = ctx.font;
2136
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2137
+ for (const level of levelLines) {
2138
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2139
+ const textWidth = ctx.measureText(labelText).width;
2140
+ const padding = 4;
2141
+ const bgX = lineLeft + 4;
2142
+ const bgY = level.y - 9;
2143
+ ctx.fillStyle = mergedOptions.backgroundColor;
2144
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2145
+ ctx.fillStyle = level.color;
2146
+ ctx.textAlign = "left";
2147
+ ctx.textBaseline = "middle";
2148
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2149
+ }
2150
+ ctx.font = prevFont;
2078
2151
  }
2079
2152
  }
2080
2153
  ctx.restore();
@@ -3162,6 +3235,28 @@ function createChart(element, options = {}) {
3162
3235
  if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3163
3236
  return { drawing, target: "line" };
3164
3237
  }
3238
+ } else if (drawing.type === "fib-retracement") {
3239
+ const first = drawing.points[0];
3240
+ const second = drawing.points[1];
3241
+ if (!first || !second) continue;
3242
+ const x1 = canvasXFromDrawingPoint(first);
3243
+ const y1 = canvasYFromDrawingPrice(first.price);
3244
+ const x2 = canvasXFromDrawingPoint(second);
3245
+ const y2 = canvasYFromDrawingPrice(second.price);
3246
+ if (Math.hypot(x - x1, y - y1) <= 9) {
3247
+ return { drawing, target: "handle", pointIndex: 0 };
3248
+ }
3249
+ if (Math.hypot(x - x2, y - y2) <= 9) {
3250
+ return { drawing, target: "handle", pointIndex: 1 };
3251
+ }
3252
+ const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
3253
+ for (const ratio of fibRatios) {
3254
+ const price = first.price + (second.price - first.price) * (1 - ratio);
3255
+ const lineY = canvasYFromDrawingPrice(price);
3256
+ if (Math.abs(y - lineY) <= 5) {
3257
+ return { drawing, target: "line" };
3258
+ }
3259
+ }
3165
3260
  }
3166
3261
  }
3167
3262
  return null;
@@ -3263,6 +3358,29 @@ function createChart(element, options = {}) {
3263
3358
  draw();
3264
3359
  return true;
3265
3360
  }
3361
+ if (activeDrawingTool === "fib-retracement") {
3362
+ if (draftDrawing?.type === "fib-retracement") {
3363
+ const completed = normalizeDrawingState({
3364
+ ...serializeDrawing(draftDrawing),
3365
+ points: [draftDrawing.points[0], point]
3366
+ });
3367
+ drawings.push(completed);
3368
+ draftDrawing = null;
3369
+ activeDrawingTool = null;
3370
+ emitDrawingsChange();
3371
+ draw();
3372
+ return true;
3373
+ }
3374
+ draftDrawing = normalizeDrawingState({
3375
+ type: "fib-retracement",
3376
+ points: [point, point],
3377
+ color: "#2563eb",
3378
+ style: "solid",
3379
+ width: 1
3380
+ });
3381
+ draw();
3382
+ return true;
3383
+ }
3266
3384
  return false;
3267
3385
  };
3268
3386
  const updateDrawingDrag = (x, y) => {
@@ -3498,7 +3616,7 @@ function createChart(element, options = {}) {
3498
3616
  setCrosshairPoint(null);
3499
3617
  return;
3500
3618
  }
3501
- if (draftDrawing && activeDrawingTool === "trendline") {
3619
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "fib-retracement")) {
3502
3620
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3503
3621
  if (nextPoint) {
3504
3622
  draftDrawing = {
package/dist/index.cjs CHANGED
@@ -2021,6 +2021,16 @@ function createChart(element, options = {}) {
2021
2021
  return chartBottom - (price - yMin) / yRange * chartHeight;
2022
2022
  };
2023
2023
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2024
+ const FIB_LEVELS = [
2025
+ { ratio: 0, color: "#787b86" },
2026
+ { ratio: 0.236, color: "#f23645", band: "rgba(242,54,69,0.10)" },
2027
+ { ratio: 0.382, color: "#ff9800", band: "rgba(255,152,0,0.10)" },
2028
+ { ratio: 0.5, color: "#4caf50", band: "rgba(76,175,80,0.10)" },
2029
+ { ratio: 0.618, color: "#089981", band: "rgba(8,153,129,0.10)" },
2030
+ { ratio: 0.786, color: "#00bcd4", band: "rgba(0,188,212,0.10)" },
2031
+ { ratio: 1, color: "#787b86", band: "rgba(120,123,134,0.10)" },
2032
+ { ratio: 1.618, color: "#2962ff", band: "rgba(41,98,255,0.08)" }
2033
+ ];
2024
2034
  const drawDrawingHandle = (x, y, color) => {
2025
2035
  ctx.save();
2026
2036
  ctx.setLineDash([]);
@@ -2033,27 +2043,24 @@ function createChart(element, options = {}) {
2033
2043
  ctx.stroke();
2034
2044
  ctx.restore();
2035
2045
  };
2036
- const drawDrawingLabel = (labelText, anchorX, anchorY, color, angle) => {
2046
+ const drawDrawingLabel = (labelText, anchorX, anchorY, color) => {
2037
2047
  if (!labelText) return;
2038
2048
  ctx.save();
2039
- ctx.translate(anchorX, anchorY);
2040
- ctx.rotate(angle);
2041
- ctx.translate(0, -8);
2042
2049
  const prevFont = ctx.font;
2043
2050
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2044
2051
  const textWidth = ctx.measureText(labelText).width;
2045
2052
  const paddingX = 6;
2046
- const paddingY = 3;
2047
2053
  const bgWidth = textWidth + paddingX * 2;
2048
2054
  const bgHeight = 18;
2055
+ const bgX = anchorX - bgWidth / 2;
2056
+ const bgY = anchorY - bgHeight - 6;
2049
2057
  ctx.fillStyle = `${color}1f`;
2050
- fillRoundedRect(-bgWidth / 2, -bgHeight, bgWidth, bgHeight, 4);
2058
+ fillRoundedRect(bgX, bgY, bgWidth, bgHeight, 4);
2051
2059
  ctx.fillStyle = color;
2052
2060
  ctx.textAlign = "center";
2053
2061
  ctx.textBaseline = "middle";
2054
- ctx.fillText(labelText, 0, -bgHeight / 2);
2062
+ ctx.fillText(labelText, anchorX, bgY + bgHeight / 2);
2055
2063
  ctx.font = prevFont;
2056
- void paddingY;
2057
2064
  ctx.restore();
2058
2065
  };
2059
2066
  const drawDrawing = (drawing, draft = false) => {
@@ -2076,7 +2083,8 @@ function createChart(element, options = {}) {
2076
2083
  ctx.stroke();
2077
2084
  drawDrawingHandle(handleX, y, drawing.color);
2078
2085
  if (drawing.label) {
2079
- drawDrawingLabel(drawing.label, chartLeft + 60, y, drawing.color, 0);
2086
+ const midX = (chartLeft + chartRight) / 2;
2087
+ drawDrawingLabel(drawing.label, midX, y, drawing.color);
2080
2088
  }
2081
2089
  }
2082
2090
  } else if (drawing.type === "trendline") {
@@ -2096,9 +2104,74 @@ function createChart(element, options = {}) {
2096
2104
  if (drawing.label) {
2097
2105
  const midX = (firstX + secondX) / 2;
2098
2106
  const midY = (firstY + secondY) / 2;
2099
- const angle = Math.atan2(secondY - firstY, secondX - firstX);
2100
- drawDrawingLabel(drawing.label, midX, midY, drawing.color, angle);
2107
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
2108
+ }
2109
+ }
2110
+ } else if (drawing.type === "fib-retracement") {
2111
+ const first = drawing.points[0];
2112
+ const second = drawing.points[1];
2113
+ if (first && second) {
2114
+ const firstX = xFromDrawingPoint(first);
2115
+ const secondX = xFromDrawingPoint(second);
2116
+ const firstY = yFromPrice(first.price);
2117
+ const secondY = yFromPrice(second.price);
2118
+ const lineLeft = chartLeft;
2119
+ const lineRight = chartRight;
2120
+ const levelLines = FIB_LEVELS.map((level) => {
2121
+ const price = first.price + (second.price - first.price) * (1 - level.ratio);
2122
+ return { ...level, price, y: yFromPrice(price) };
2123
+ });
2124
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2125
+ const top = levelLines[index];
2126
+ const bottom = levelLines[index + 1];
2127
+ const fillColor = bottom.band ?? top.band;
2128
+ if (!fillColor) continue;
2129
+ const bandTop = Math.min(top.y, bottom.y);
2130
+ const bandBottom = Math.max(top.y, bottom.y);
2131
+ ctx.save();
2132
+ ctx.fillStyle = fillColor;
2133
+ ctx.globalAlpha = draft ? 0.5 : 1;
2134
+ ctx.fillRect(lineLeft, bandTop, lineRight - lineLeft, bandBottom - bandTop);
2135
+ ctx.restore();
2136
+ }
2137
+ ctx.save();
2138
+ ctx.lineWidth = drawing.width;
2139
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2140
+ for (const level of levelLines) {
2141
+ ctx.strokeStyle = level.color;
2142
+ ctx.beginPath();
2143
+ ctx.moveTo(crisp(lineLeft), crisp(level.y));
2144
+ ctx.lineTo(crisp(lineRight), crisp(level.y));
2145
+ ctx.stroke();
2101
2146
  }
2147
+ ctx.restore();
2148
+ ctx.save();
2149
+ ctx.strokeStyle = "rgba(148,163,184,0.7)";
2150
+ ctx.setLineDash([4, 4]);
2151
+ ctx.lineWidth = 1;
2152
+ ctx.beginPath();
2153
+ ctx.moveTo(firstX, firstY);
2154
+ ctx.lineTo(secondX, secondY);
2155
+ ctx.stroke();
2156
+ ctx.restore();
2157
+ drawDrawingHandle(firstX, firstY, drawing.color);
2158
+ drawDrawingHandle(secondX, secondY, drawing.color);
2159
+ const prevFont = ctx.font;
2160
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2161
+ for (const level of levelLines) {
2162
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2163
+ const textWidth = ctx.measureText(labelText).width;
2164
+ const padding = 4;
2165
+ const bgX = lineLeft + 4;
2166
+ const bgY = level.y - 9;
2167
+ ctx.fillStyle = mergedOptions.backgroundColor;
2168
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2169
+ ctx.fillStyle = level.color;
2170
+ ctx.textAlign = "left";
2171
+ ctx.textBaseline = "middle";
2172
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2173
+ }
2174
+ ctx.font = prevFont;
2102
2175
  }
2103
2176
  }
2104
2177
  ctx.restore();
@@ -3186,6 +3259,28 @@ function createChart(element, options = {}) {
3186
3259
  if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3187
3260
  return { drawing, target: "line" };
3188
3261
  }
3262
+ } else if (drawing.type === "fib-retracement") {
3263
+ const first = drawing.points[0];
3264
+ const second = drawing.points[1];
3265
+ if (!first || !second) continue;
3266
+ const x1 = canvasXFromDrawingPoint(first);
3267
+ const y1 = canvasYFromDrawingPrice(first.price);
3268
+ const x2 = canvasXFromDrawingPoint(second);
3269
+ const y2 = canvasYFromDrawingPrice(second.price);
3270
+ if (Math.hypot(x - x1, y - y1) <= 9) {
3271
+ return { drawing, target: "handle", pointIndex: 0 };
3272
+ }
3273
+ if (Math.hypot(x - x2, y - y2) <= 9) {
3274
+ return { drawing, target: "handle", pointIndex: 1 };
3275
+ }
3276
+ const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
3277
+ for (const ratio of fibRatios) {
3278
+ const price = first.price + (second.price - first.price) * (1 - ratio);
3279
+ const lineY = canvasYFromDrawingPrice(price);
3280
+ if (Math.abs(y - lineY) <= 5) {
3281
+ return { drawing, target: "line" };
3282
+ }
3283
+ }
3189
3284
  }
3190
3285
  }
3191
3286
  return null;
@@ -3287,6 +3382,29 @@ function createChart(element, options = {}) {
3287
3382
  draw();
3288
3383
  return true;
3289
3384
  }
3385
+ if (activeDrawingTool === "fib-retracement") {
3386
+ if (draftDrawing?.type === "fib-retracement") {
3387
+ const completed = normalizeDrawingState({
3388
+ ...serializeDrawing(draftDrawing),
3389
+ points: [draftDrawing.points[0], point]
3390
+ });
3391
+ drawings.push(completed);
3392
+ draftDrawing = null;
3393
+ activeDrawingTool = null;
3394
+ emitDrawingsChange();
3395
+ draw();
3396
+ return true;
3397
+ }
3398
+ draftDrawing = normalizeDrawingState({
3399
+ type: "fib-retracement",
3400
+ points: [point, point],
3401
+ color: "#2563eb",
3402
+ style: "solid",
3403
+ width: 1
3404
+ });
3405
+ draw();
3406
+ return true;
3407
+ }
3290
3408
  return false;
3291
3409
  };
3292
3410
  const updateDrawingDrag = (x, y) => {
@@ -3522,7 +3640,7 @@ function createChart(element, options = {}) {
3522
3640
  setCrosshairPoint(null);
3523
3641
  return;
3524
3642
  }
3525
- if (draftDrawing && activeDrawingTool === "trendline") {
3643
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "fib-retracement")) {
3526
3644
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3527
3645
  if (nextPoint) {
3528
3646
  draftDrawing = {
package/dist/index.d.cts CHANGED
@@ -43,7 +43,7 @@ interface ChartOptions {
43
43
  drawings?: DrawingObjectOptions[];
44
44
  }
45
45
  type IndicatorPane = "overlay" | "separate";
46
- type DrawingToolType = "horizontal-line" | "trendline";
46
+ type DrawingToolType = "horizontal-line" | "trendline" | "fib-retracement";
47
47
  interface DrawingPoint {
48
48
  index: number;
49
49
  price: number;
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ interface ChartOptions {
43
43
  drawings?: DrawingObjectOptions[];
44
44
  }
45
45
  type IndicatorPane = "overlay" | "separate";
46
- type DrawingToolType = "horizontal-line" | "trendline";
46
+ type DrawingToolType = "horizontal-line" | "trendline" | "fib-retracement";
47
47
  interface DrawingPoint {
48
48
  index: number;
49
49
  price: number;
package/dist/index.js CHANGED
@@ -1997,6 +1997,16 @@ function createChart(element, options = {}) {
1997
1997
  return chartBottom - (price - yMin) / yRange * chartHeight;
1998
1998
  };
1999
1999
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2000
+ const FIB_LEVELS = [
2001
+ { ratio: 0, color: "#787b86" },
2002
+ { ratio: 0.236, color: "#f23645", band: "rgba(242,54,69,0.10)" },
2003
+ { ratio: 0.382, color: "#ff9800", band: "rgba(255,152,0,0.10)" },
2004
+ { ratio: 0.5, color: "#4caf50", band: "rgba(76,175,80,0.10)" },
2005
+ { ratio: 0.618, color: "#089981", band: "rgba(8,153,129,0.10)" },
2006
+ { ratio: 0.786, color: "#00bcd4", band: "rgba(0,188,212,0.10)" },
2007
+ { ratio: 1, color: "#787b86", band: "rgba(120,123,134,0.10)" },
2008
+ { ratio: 1.618, color: "#2962ff", band: "rgba(41,98,255,0.08)" }
2009
+ ];
2000
2010
  const drawDrawingHandle = (x, y, color) => {
2001
2011
  ctx.save();
2002
2012
  ctx.setLineDash([]);
@@ -2009,27 +2019,24 @@ function createChart(element, options = {}) {
2009
2019
  ctx.stroke();
2010
2020
  ctx.restore();
2011
2021
  };
2012
- const drawDrawingLabel = (labelText, anchorX, anchorY, color, angle) => {
2022
+ const drawDrawingLabel = (labelText, anchorX, anchorY, color) => {
2013
2023
  if (!labelText) return;
2014
2024
  ctx.save();
2015
- ctx.translate(anchorX, anchorY);
2016
- ctx.rotate(angle);
2017
- ctx.translate(0, -8);
2018
2025
  const prevFont = ctx.font;
2019
2026
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2020
2027
  const textWidth = ctx.measureText(labelText).width;
2021
2028
  const paddingX = 6;
2022
- const paddingY = 3;
2023
2029
  const bgWidth = textWidth + paddingX * 2;
2024
2030
  const bgHeight = 18;
2031
+ const bgX = anchorX - bgWidth / 2;
2032
+ const bgY = anchorY - bgHeight - 6;
2025
2033
  ctx.fillStyle = `${color}1f`;
2026
- fillRoundedRect(-bgWidth / 2, -bgHeight, bgWidth, bgHeight, 4);
2034
+ fillRoundedRect(bgX, bgY, bgWidth, bgHeight, 4);
2027
2035
  ctx.fillStyle = color;
2028
2036
  ctx.textAlign = "center";
2029
2037
  ctx.textBaseline = "middle";
2030
- ctx.fillText(labelText, 0, -bgHeight / 2);
2038
+ ctx.fillText(labelText, anchorX, bgY + bgHeight / 2);
2031
2039
  ctx.font = prevFont;
2032
- void paddingY;
2033
2040
  ctx.restore();
2034
2041
  };
2035
2042
  const drawDrawing = (drawing, draft = false) => {
@@ -2052,7 +2059,8 @@ function createChart(element, options = {}) {
2052
2059
  ctx.stroke();
2053
2060
  drawDrawingHandle(handleX, y, drawing.color);
2054
2061
  if (drawing.label) {
2055
- drawDrawingLabel(drawing.label, chartLeft + 60, y, drawing.color, 0);
2062
+ const midX = (chartLeft + chartRight) / 2;
2063
+ drawDrawingLabel(drawing.label, midX, y, drawing.color);
2056
2064
  }
2057
2065
  }
2058
2066
  } else if (drawing.type === "trendline") {
@@ -2072,9 +2080,74 @@ function createChart(element, options = {}) {
2072
2080
  if (drawing.label) {
2073
2081
  const midX = (firstX + secondX) / 2;
2074
2082
  const midY = (firstY + secondY) / 2;
2075
- const angle = Math.atan2(secondY - firstY, secondX - firstX);
2076
- drawDrawingLabel(drawing.label, midX, midY, drawing.color, angle);
2083
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
2084
+ }
2085
+ }
2086
+ } else if (drawing.type === "fib-retracement") {
2087
+ const first = drawing.points[0];
2088
+ const second = drawing.points[1];
2089
+ if (first && second) {
2090
+ const firstX = xFromDrawingPoint(first);
2091
+ const secondX = xFromDrawingPoint(second);
2092
+ const firstY = yFromPrice(first.price);
2093
+ const secondY = yFromPrice(second.price);
2094
+ const lineLeft = chartLeft;
2095
+ const lineRight = chartRight;
2096
+ const levelLines = FIB_LEVELS.map((level) => {
2097
+ const price = first.price + (second.price - first.price) * (1 - level.ratio);
2098
+ return { ...level, price, y: yFromPrice(price) };
2099
+ });
2100
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2101
+ const top = levelLines[index];
2102
+ const bottom = levelLines[index + 1];
2103
+ const fillColor = bottom.band ?? top.band;
2104
+ if (!fillColor) continue;
2105
+ const bandTop = Math.min(top.y, bottom.y);
2106
+ const bandBottom = Math.max(top.y, bottom.y);
2107
+ ctx.save();
2108
+ ctx.fillStyle = fillColor;
2109
+ ctx.globalAlpha = draft ? 0.5 : 1;
2110
+ ctx.fillRect(lineLeft, bandTop, lineRight - lineLeft, bandBottom - bandTop);
2111
+ ctx.restore();
2112
+ }
2113
+ ctx.save();
2114
+ ctx.lineWidth = drawing.width;
2115
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2116
+ for (const level of levelLines) {
2117
+ ctx.strokeStyle = level.color;
2118
+ ctx.beginPath();
2119
+ ctx.moveTo(crisp(lineLeft), crisp(level.y));
2120
+ ctx.lineTo(crisp(lineRight), crisp(level.y));
2121
+ ctx.stroke();
2077
2122
  }
2123
+ ctx.restore();
2124
+ ctx.save();
2125
+ ctx.strokeStyle = "rgba(148,163,184,0.7)";
2126
+ ctx.setLineDash([4, 4]);
2127
+ ctx.lineWidth = 1;
2128
+ ctx.beginPath();
2129
+ ctx.moveTo(firstX, firstY);
2130
+ ctx.lineTo(secondX, secondY);
2131
+ ctx.stroke();
2132
+ ctx.restore();
2133
+ drawDrawingHandle(firstX, firstY, drawing.color);
2134
+ drawDrawingHandle(secondX, secondY, drawing.color);
2135
+ const prevFont = ctx.font;
2136
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2137
+ for (const level of levelLines) {
2138
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2139
+ const textWidth = ctx.measureText(labelText).width;
2140
+ const padding = 4;
2141
+ const bgX = lineLeft + 4;
2142
+ const bgY = level.y - 9;
2143
+ ctx.fillStyle = mergedOptions.backgroundColor;
2144
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2145
+ ctx.fillStyle = level.color;
2146
+ ctx.textAlign = "left";
2147
+ ctx.textBaseline = "middle";
2148
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2149
+ }
2150
+ ctx.font = prevFont;
2078
2151
  }
2079
2152
  }
2080
2153
  ctx.restore();
@@ -3162,6 +3235,28 @@ function createChart(element, options = {}) {
3162
3235
  if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3163
3236
  return { drawing, target: "line" };
3164
3237
  }
3238
+ } else if (drawing.type === "fib-retracement") {
3239
+ const first = drawing.points[0];
3240
+ const second = drawing.points[1];
3241
+ if (!first || !second) continue;
3242
+ const x1 = canvasXFromDrawingPoint(first);
3243
+ const y1 = canvasYFromDrawingPrice(first.price);
3244
+ const x2 = canvasXFromDrawingPoint(second);
3245
+ const y2 = canvasYFromDrawingPrice(second.price);
3246
+ if (Math.hypot(x - x1, y - y1) <= 9) {
3247
+ return { drawing, target: "handle", pointIndex: 0 };
3248
+ }
3249
+ if (Math.hypot(x - x2, y - y2) <= 9) {
3250
+ return { drawing, target: "handle", pointIndex: 1 };
3251
+ }
3252
+ const fibRatios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
3253
+ for (const ratio of fibRatios) {
3254
+ const price = first.price + (second.price - first.price) * (1 - ratio);
3255
+ const lineY = canvasYFromDrawingPrice(price);
3256
+ if (Math.abs(y - lineY) <= 5) {
3257
+ return { drawing, target: "line" };
3258
+ }
3259
+ }
3165
3260
  }
3166
3261
  }
3167
3262
  return null;
@@ -3263,6 +3358,29 @@ function createChart(element, options = {}) {
3263
3358
  draw();
3264
3359
  return true;
3265
3360
  }
3361
+ if (activeDrawingTool === "fib-retracement") {
3362
+ if (draftDrawing?.type === "fib-retracement") {
3363
+ const completed = normalizeDrawingState({
3364
+ ...serializeDrawing(draftDrawing),
3365
+ points: [draftDrawing.points[0], point]
3366
+ });
3367
+ drawings.push(completed);
3368
+ draftDrawing = null;
3369
+ activeDrawingTool = null;
3370
+ emitDrawingsChange();
3371
+ draw();
3372
+ return true;
3373
+ }
3374
+ draftDrawing = normalizeDrawingState({
3375
+ type: "fib-retracement",
3376
+ points: [point, point],
3377
+ color: "#2563eb",
3378
+ style: "solid",
3379
+ width: 1
3380
+ });
3381
+ draw();
3382
+ return true;
3383
+ }
3266
3384
  return false;
3267
3385
  };
3268
3386
  const updateDrawingDrag = (x, y) => {
@@ -3498,7 +3616,7 @@ function createChart(element, options = {}) {
3498
3616
  setCrosshairPoint(null);
3499
3617
  return;
3500
3618
  }
3501
- if (draftDrawing && activeDrawingTool === "trendline") {
3619
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "fib-retracement")) {
3502
3620
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3503
3621
  if (nextPoint) {
3504
3622
  draftDrawing = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",