hyperprop-charting-library 0.1.135 → 0.1.136

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.
@@ -4818,6 +4818,216 @@ function createChart(element, options = {}) {
4818
4818
  drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
4819
4819
  }
4820
4820
  }
4821
+ } else if (drawing.type === "ellipse") {
4822
+ const p0 = drawing.points[0];
4823
+ const p1 = drawing.points[1];
4824
+ if (p0 && p1) {
4825
+ const px0 = xFromDrawingPoint(p0);
4826
+ const px1 = xFromDrawingPoint(p1);
4827
+ const y0 = yFromPrice(p0.price);
4828
+ const y1 = yFromPrice(p1.price);
4829
+ const cx = (px0 + px1) / 2;
4830
+ const cy = (y0 + y1) / 2;
4831
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
4832
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
4833
+ ctx.save();
4834
+ ctx.globalAlpha = draft ? 0.6 : 1;
4835
+ ctx.beginPath();
4836
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
4837
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
4838
+ ctx.fill();
4839
+ ctx.lineWidth = Math.max(1, drawing.width);
4840
+ ctx.strokeStyle = drawing.color;
4841
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
4842
+ ctx.stroke();
4843
+ ctx.restore();
4844
+ handleAt(px0, y0, drawing.color);
4845
+ handleAt(px1, y1, drawing.color);
4846
+ handleAt(px0, y1, drawing.color);
4847
+ handleAt(px1, y0, drawing.color);
4848
+ if (drawing.label) {
4849
+ drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
4850
+ }
4851
+ }
4852
+ } else if (drawing.type === "parallel-channel") {
4853
+ const p0 = drawing.points[0];
4854
+ const p1 = drawing.points[1];
4855
+ const p2 = drawing.points[2];
4856
+ if (p0 && p1) {
4857
+ const x0 = xFromDrawingPoint(p0);
4858
+ const y0 = yFromPrice(p0.price);
4859
+ const x1 = xFromDrawingPoint(p1);
4860
+ const y1 = yFromPrice(p1.price);
4861
+ ctx.beginPath();
4862
+ ctx.moveTo(x0, y0);
4863
+ ctx.lineTo(x1, y1);
4864
+ ctx.stroke();
4865
+ handleAt(x0, y0, drawing.color);
4866
+ handleAt(x1, y1, drawing.color);
4867
+ if (p2) {
4868
+ const indexSpan = p1.index - p0.index;
4869
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
4870
+ const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
4871
+ const offset = p2.price - basePriceAtP2;
4872
+ const q0x = x0;
4873
+ const q0y = yFromPrice(p0.price + offset);
4874
+ const q1x = x1;
4875
+ const q1y = yFromPrice(p1.price + offset);
4876
+ ctx.save();
4877
+ ctx.globalAlpha = draft ? 0.5 : 1;
4878
+ ctx.fillStyle = hexToRgba(drawing.color, 0.08);
4879
+ ctx.beginPath();
4880
+ ctx.moveTo(x0, y0);
4881
+ ctx.lineTo(x1, y1);
4882
+ ctx.lineTo(q1x, q1y);
4883
+ ctx.lineTo(q0x, q0y);
4884
+ ctx.closePath();
4885
+ ctx.fill();
4886
+ ctx.restore();
4887
+ ctx.beginPath();
4888
+ ctx.moveTo(q0x, q0y);
4889
+ ctx.lineTo(q1x, q1y);
4890
+ ctx.stroke();
4891
+ ctx.save();
4892
+ ctx.setLineDash([4, 4]);
4893
+ ctx.globalAlpha = draft ? 0.5 : 0.7;
4894
+ ctx.beginPath();
4895
+ ctx.moveTo(x0, (y0 + q0y) / 2);
4896
+ ctx.lineTo(x1, (y1 + q1y) / 2);
4897
+ ctx.stroke();
4898
+ ctx.restore();
4899
+ handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
4900
+ }
4901
+ if (drawing.label) {
4902
+ drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
4903
+ }
4904
+ }
4905
+ } else if (drawing.type === "arrow") {
4906
+ const first = drawing.points[0];
4907
+ const second = drawing.points[1];
4908
+ if (first && second) {
4909
+ const x0 = xFromDrawingPoint(first);
4910
+ const y0 = yFromPrice(first.price);
4911
+ const x1 = xFromDrawingPoint(second);
4912
+ const y1 = yFromPrice(second.price);
4913
+ const angle = Math.atan2(y1 - y0, x1 - x0);
4914
+ const head = 5 + Math.max(1, drawing.width) * 3;
4915
+ const shaftX = x1 - Math.cos(angle) * head * 0.6;
4916
+ const shaftY = y1 - Math.sin(angle) * head * 0.6;
4917
+ ctx.beginPath();
4918
+ ctx.moveTo(x0, y0);
4919
+ ctx.lineTo(shaftX, shaftY);
4920
+ ctx.stroke();
4921
+ ctx.save();
4922
+ ctx.setLineDash([]);
4923
+ ctx.fillStyle = drawing.color;
4924
+ ctx.beginPath();
4925
+ ctx.moveTo(x1, y1);
4926
+ ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
4927
+ ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
4928
+ ctx.closePath();
4929
+ ctx.fill();
4930
+ ctx.restore();
4931
+ handleAt(x0, y0, drawing.color);
4932
+ handleAt(x1, y1, drawing.color);
4933
+ if (drawing.label) {
4934
+ const midX = (x0 + x1) / 2;
4935
+ const midY = (y0 + y1) / 2;
4936
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
4937
+ }
4938
+ }
4939
+ } else if (drawing.type === "brush") {
4940
+ const pts = drawing.points;
4941
+ if (pts.length >= 2) {
4942
+ ctx.save();
4943
+ ctx.lineJoin = "round";
4944
+ ctx.lineCap = "round";
4945
+ ctx.beginPath();
4946
+ const startX = xFromDrawingPoint(pts[0]);
4947
+ const startY = yFromPrice(pts[0].price);
4948
+ ctx.moveTo(startX, startY);
4949
+ if (pts.length === 2) {
4950
+ ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
4951
+ } else {
4952
+ for (let i = 1; i < pts.length - 1; i += 1) {
4953
+ const cxp = xFromDrawingPoint(pts[i]);
4954
+ const cyp = yFromPrice(pts[i].price);
4955
+ const nx = xFromDrawingPoint(pts[i + 1]);
4956
+ const ny = yFromPrice(pts[i + 1].price);
4957
+ ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
4958
+ }
4959
+ const last = pts[pts.length - 1];
4960
+ ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
4961
+ }
4962
+ ctx.stroke();
4963
+ ctx.restore();
4964
+ if (isSelected) {
4965
+ handleAt(startX, startY, drawing.color);
4966
+ const last = pts[pts.length - 1];
4967
+ handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
4968
+ }
4969
+ }
4970
+ } else if (drawing.type === "callout") {
4971
+ const anchor = drawing.points[0];
4972
+ const boxPoint = drawing.points[1];
4973
+ if (anchor && boxPoint) {
4974
+ const ax = xFromDrawingPoint(anchor);
4975
+ const ay = yFromPrice(anchor.price);
4976
+ const bx = xFromDrawingPoint(boxPoint);
4977
+ const by = yFromPrice(boxPoint.price);
4978
+ const fontSize = Math.max(6, drawing.fontSize);
4979
+ const prevFont = ctx.font;
4980
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
4981
+ const rawLabel = drawing.label ?? "";
4982
+ const isPlaceholder = rawLabel.trim().length === 0;
4983
+ const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
4984
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
4985
+ const padX = 10;
4986
+ const padY = 7;
4987
+ const lineH = Math.round(fontSize * 1.35);
4988
+ const blockW = textW + padX * 2;
4989
+ const blockH = lines.length * lineH + padY * 2;
4990
+ const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
4991
+ const edgeY = Math.max(by, Math.min(by + blockH, ay));
4992
+ const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
4993
+ if (!insideBox) {
4994
+ ctx.save();
4995
+ ctx.setLineDash([]);
4996
+ ctx.lineWidth = Math.max(1, drawing.width);
4997
+ ctx.beginPath();
4998
+ ctx.moveTo(edgeX, edgeY);
4999
+ ctx.lineTo(ax, ay);
5000
+ ctx.stroke();
5001
+ const angle = Math.atan2(ay - edgeY, ax - edgeX);
5002
+ const head = 7;
5003
+ ctx.fillStyle = drawing.color;
5004
+ ctx.beginPath();
5005
+ ctx.moveTo(ax, ay);
5006
+ ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
5007
+ ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
5008
+ ctx.closePath();
5009
+ ctx.fill();
5010
+ ctx.restore();
5011
+ }
5012
+ ctx.save();
5013
+ ctx.setLineDash([]);
5014
+ ctx.fillStyle = hexToRgba(drawing.color, 0.16);
5015
+ fillRoundedRect(bx, by, blockW, blockH, 6);
5016
+ ctx.strokeStyle = drawing.color;
5017
+ ctx.lineWidth = Math.max(1, drawing.width);
5018
+ strokeRoundedRect(bx, by, blockW, blockH, 6);
5019
+ ctx.fillStyle = drawing.color;
5020
+ ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
5021
+ ctx.textAlign = "left";
5022
+ ctx.textBaseline = "middle";
5023
+ lines.forEach((line, lineIndex) => {
5024
+ ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
5025
+ });
5026
+ ctx.restore();
5027
+ ctx.font = prevFont;
5028
+ handleAt(ax, ay, drawing.color);
5029
+ handleAt(bx, by, drawing.color);
5030
+ }
4821
5031
  } else if (drawing.type === "price-range") {
4822
5032
  const p0 = drawing.points[0];
4823
5033
  const p1 = drawing.points[1];
@@ -6872,6 +7082,126 @@ function createChart(element, options = {}) {
6872
7082
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
6873
7083
  return { drawing, target: "line" };
6874
7084
  }
7085
+ } else if (drawing.type === "ellipse") {
7086
+ const p0 = drawing.points[0];
7087
+ const p1 = drawing.points[1];
7088
+ if (!p0 || !p1) continue;
7089
+ const px0 = canvasXFromDrawingPoint(p0);
7090
+ const px1 = canvasXFromDrawingPoint(p1);
7091
+ const y0 = canvasYFromDrawingPrice(p0.price);
7092
+ const y1 = canvasYFromDrawingPrice(p1.price);
7093
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7094
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7095
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7096
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7097
+ const cx = (px0 + px1) / 2;
7098
+ const cy = (y0 + y1) / 2;
7099
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7100
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7101
+ const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7102
+ if (norm <= 1) {
7103
+ return { drawing, target: "line" };
7104
+ }
7105
+ } else if (drawing.type === "parallel-channel") {
7106
+ const p0 = drawing.points[0];
7107
+ const p1 = drawing.points[1];
7108
+ const p2 = drawing.points[2];
7109
+ if (!p0 || !p1) continue;
7110
+ const x0 = canvasXFromDrawingPoint(p0);
7111
+ const y0 = canvasYFromDrawingPrice(p0.price);
7112
+ const x1 = canvasXFromDrawingPoint(p1);
7113
+ const y1 = canvasYFromDrawingPrice(p1.price);
7114
+ if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7115
+ if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7116
+ if (p2) {
7117
+ const x2 = canvasXFromDrawingPoint(p2);
7118
+ const y2 = canvasYFromDrawingPrice(p2.price);
7119
+ if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7120
+ const indexSpan = p1.index - p0.index;
7121
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7122
+ const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7123
+ const q0y = canvasYFromDrawingPrice(p0.price + offset);
7124
+ const q1y = canvasYFromDrawingPrice(p1.price + offset);
7125
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7126
+ return { drawing, target: "line" };
7127
+ }
7128
+ const minX = Math.min(x0, x1);
7129
+ const maxX = Math.max(x0, x1);
7130
+ if (x >= minX && x <= maxX && maxX > minX) {
7131
+ const t = (x - x0) / (x1 - x0);
7132
+ const baseY = y0 + t * (y1 - y0);
7133
+ const parY = q0y + t * (q1y - q0y);
7134
+ if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
7135
+ return { drawing, target: "line" };
7136
+ }
7137
+ }
7138
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7139
+ return { drawing, target: "line" };
7140
+ }
7141
+ } else if (drawing.type === "arrow") {
7142
+ const first = drawing.points[0];
7143
+ const second = drawing.points[1];
7144
+ if (!first || !second) continue;
7145
+ const x1 = canvasXFromDrawingPoint(first);
7146
+ const y1 = canvasYFromDrawingPrice(first.price);
7147
+ const x2 = canvasXFromDrawingPoint(second);
7148
+ const y2 = canvasYFromDrawingPrice(second.price);
7149
+ if (Math.hypot(x - x1, y - y1) <= 8) {
7150
+ return { drawing, target: "handle", pointIndex: 0 };
7151
+ }
7152
+ if (Math.hypot(x - x2, y - y2) <= 8) {
7153
+ return { drawing, target: "handle", pointIndex: 1 };
7154
+ }
7155
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7156
+ return { drawing, target: "line" };
7157
+ }
7158
+ } else if (drawing.type === "brush") {
7159
+ const pts = drawing.points;
7160
+ if (pts.length < 2) continue;
7161
+ let hitBody = false;
7162
+ let prevX = canvasXFromDrawingPoint(pts[0]);
7163
+ let prevY = canvasYFromDrawingPrice(pts[0].price);
7164
+ for (let i = 1; i < pts.length; i += 1) {
7165
+ const nextX = canvasXFromDrawingPoint(pts[i]);
7166
+ const nextY = canvasYFromDrawingPrice(pts[i].price);
7167
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7168
+ hitBody = true;
7169
+ break;
7170
+ }
7171
+ prevX = nextX;
7172
+ prevY = nextY;
7173
+ }
7174
+ if (hitBody) {
7175
+ return { drawing, target: "line" };
7176
+ }
7177
+ } else if (drawing.type === "callout") {
7178
+ const anchor = drawing.points[0];
7179
+ const boxPoint = drawing.points[1];
7180
+ if (!anchor || !boxPoint) continue;
7181
+ const ax = canvasXFromDrawingPoint(anchor);
7182
+ const ay = canvasYFromDrawingPrice(anchor.price);
7183
+ const bx = canvasXFromDrawingPoint(boxPoint);
7184
+ const by = canvasYFromDrawingPrice(boxPoint.price);
7185
+ if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7186
+ if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7187
+ const fontSize = Math.max(6, drawing.fontSize);
7188
+ const prevFont = ctx.font;
7189
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
7190
+ const rawLabel = drawing.label ?? "";
7191
+ const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
7192
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
7193
+ ctx.font = prevFont;
7194
+ const padX = 10;
7195
+ const padY = 7;
7196
+ const lineH = Math.round(fontSize * 1.35);
7197
+ const blockW = textW + padX * 2;
7198
+ const blockH = lines.length * lineH + padY * 2;
7199
+ if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7200
+ return { drawing, target: "line" };
7201
+ }
7202
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7203
+ return { drawing, target: "line" };
7204
+ }
6875
7205
  } else if (drawing.type === "price-range") {
6876
7206
  const p0 = drawing.points[0];
6877
7207
  const p1 = drawing.points[1];
@@ -7100,6 +7430,128 @@ function createChart(element, options = {}) {
7100
7430
  scheduleDraw();
7101
7431
  return true;
7102
7432
  }
7433
+ if (activeDrawingTool === "arrow") {
7434
+ if (draftDrawing?.type === "arrow") {
7435
+ const anchor = draftDrawing.points[0];
7436
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
7437
+ const completed = normalizeDrawingState({
7438
+ ...serializeDrawing(draftDrawing),
7439
+ points: [anchor, endPoint]
7440
+ });
7441
+ drawings.push(completed);
7442
+ draftDrawing = null;
7443
+ activeDrawingTool = null;
7444
+ emitDrawingsChange();
7445
+ scheduleDraw();
7446
+ return true;
7447
+ }
7448
+ const defaults = getDrawingToolDefaults("arrow");
7449
+ draftDrawing = normalizeDrawingState({
7450
+ type: "arrow",
7451
+ points: [point, point],
7452
+ color: defaults.color ?? "#2563eb",
7453
+ style: defaults.style ?? "solid",
7454
+ width: defaults.width ?? 2
7455
+ });
7456
+ scheduleDraw();
7457
+ return true;
7458
+ }
7459
+ if (activeDrawingTool === "parallel-channel") {
7460
+ if (draftDrawing?.type === "parallel-channel") {
7461
+ if (draftDrawing.points.length < 3) {
7462
+ draftDrawing = normalizeDrawingState({
7463
+ ...serializeDrawing(draftDrawing),
7464
+ points: [...draftDrawing.points.slice(0, -1), point, point]
7465
+ });
7466
+ scheduleDraw();
7467
+ return true;
7468
+ }
7469
+ const completed = normalizeDrawingState({
7470
+ ...serializeDrawing(draftDrawing),
7471
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
7472
+ });
7473
+ drawings.push(completed);
7474
+ draftDrawing = null;
7475
+ activeDrawingTool = null;
7476
+ emitDrawingsChange();
7477
+ scheduleDraw();
7478
+ return true;
7479
+ }
7480
+ const defaults = getDrawingToolDefaults("parallel-channel");
7481
+ draftDrawing = normalizeDrawingState({
7482
+ type: "parallel-channel",
7483
+ points: [point, point],
7484
+ color: defaults.color ?? "#2563eb",
7485
+ style: defaults.style ?? "solid",
7486
+ width: defaults.width ?? 2
7487
+ });
7488
+ scheduleDraw();
7489
+ return true;
7490
+ }
7491
+ if (activeDrawingTool === "ellipse") {
7492
+ if (draftDrawing?.type === "ellipse") {
7493
+ const completed = normalizeDrawingState({
7494
+ ...serializeDrawing(draftDrawing),
7495
+ points: [draftDrawing.points[0], point]
7496
+ });
7497
+ drawings.push(completed);
7498
+ draftDrawing = null;
7499
+ activeDrawingTool = null;
7500
+ emitDrawingsChange();
7501
+ scheduleDraw();
7502
+ return true;
7503
+ }
7504
+ const defaults = getDrawingToolDefaults("ellipse");
7505
+ draftDrawing = normalizeDrawingState({
7506
+ type: "ellipse",
7507
+ points: [point, point],
7508
+ color: defaults.color ?? "#2962ff",
7509
+ style: defaults.style ?? "solid",
7510
+ width: defaults.width ?? 1
7511
+ });
7512
+ scheduleDraw();
7513
+ return true;
7514
+ }
7515
+ if (activeDrawingTool === "callout") {
7516
+ if (draftDrawing?.type === "callout") {
7517
+ const completed = normalizeDrawingState({
7518
+ ...serializeDrawing(draftDrawing),
7519
+ points: [draftDrawing.points[0], point]
7520
+ });
7521
+ drawings.push(completed);
7522
+ draftDrawing = null;
7523
+ activeDrawingTool = null;
7524
+ selectedDrawingId = completed.id;
7525
+ emitDrawingsChange();
7526
+ scheduleDraw();
7527
+ drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
7528
+ return true;
7529
+ }
7530
+ const defaults = getDrawingToolDefaults("callout");
7531
+ draftDrawing = normalizeDrawingState({
7532
+ type: "callout",
7533
+ points: [point, point],
7534
+ color: defaults.color ?? "#2962ff",
7535
+ style: defaults.style ?? "solid",
7536
+ width: defaults.width ?? 1,
7537
+ ...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
7538
+ label: ""
7539
+ });
7540
+ scheduleDraw();
7541
+ return true;
7542
+ }
7543
+ if (activeDrawingTool === "brush") {
7544
+ const defaults = getDrawingToolDefaults("brush");
7545
+ draftDrawing = normalizeDrawingState({
7546
+ type: "brush",
7547
+ points: [point],
7548
+ color: defaults.color ?? "#f59e0b",
7549
+ style: defaults.style ?? "solid",
7550
+ width: defaults.width ?? 2
7551
+ });
7552
+ scheduleDraw();
7553
+ return true;
7554
+ }
7103
7555
  if (activeDrawingTool === "fib-retracement") {
7104
7556
  if (draftDrawing?.type === "fib-retracement") {
7105
7557
  const completed = normalizeDrawingState({
@@ -7303,7 +7755,7 @@ function createChart(element, options = {}) {
7303
7755
  }
7304
7756
  return { ...drawing, points: pts };
7305
7757
  }
7306
- if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
7758
+ if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
7307
7759
  const pts = drawing.points.map((point) => ({ ...point }));
7308
7760
  const p0 = pts[0];
7309
7761
  const p1 = pts[1];
@@ -7322,7 +7774,7 @@ function createChart(element, options = {}) {
7322
7774
  }
7323
7775
  return { ...drawing, points: pts };
7324
7776
  }
7325
- if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
7777
+ if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
7326
7778
  const pointIndex = drawingDragState.pointIndex ?? 0;
7327
7779
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
7328
7780
  const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
@@ -7572,6 +8024,10 @@ function createChart(element, options = {}) {
7572
8024
  if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
7573
8025
  setCrosshairPoint(null);
7574
8026
  canvas.style.cursor = "crosshair";
8027
+ if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
8028
+ activePointerId = event.pointerId;
8029
+ capturePointer(event.pointerId);
8030
+ }
7575
8031
  return;
7576
8032
  }
7577
8033
  isDragging = true;
@@ -7656,9 +8112,30 @@ function createChart(element, options = {}) {
7656
8112
  setCrosshairPoint(null);
7657
8113
  return;
7658
8114
  }
7659
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
8115
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8116
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
8117
+ return;
8118
+ }
8119
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
8120
+ if (nextPoint) {
8121
+ const last = draftDrawing.points[draftDrawing.points.length - 1];
8122
+ const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
8123
+ const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
8124
+ if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
8125
+ draftDrawing = {
8126
+ ...draftDrawing,
8127
+ points: [...draftDrawing.points, nextPoint]
8128
+ };
8129
+ }
8130
+ canvas.style.cursor = "crosshair";
8131
+ setCrosshairPoint(null);
8132
+ scheduleDraw();
8133
+ }
8134
+ return;
8135
+ }
8136
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
7660
8137
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
7661
- if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
8138
+ if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
7662
8139
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
7663
8140
  }
7664
8141
  if (nextPoint) {
@@ -7895,6 +8372,25 @@ function createChart(element, options = {}) {
7895
8372
  scheduleDraw();
7896
8373
  return;
7897
8374
  }
8375
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8376
+ if (draftDrawing.points.length >= 2) {
8377
+ const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
8378
+ drawings.push(completed);
8379
+ draftDrawing = null;
8380
+ activeDrawingTool = null;
8381
+ canvas.style.cursor = "default";
8382
+ emitDrawingsChange();
8383
+ } else {
8384
+ draftDrawing = null;
8385
+ canvas.style.cursor = "crosshair";
8386
+ }
8387
+ isDragging = false;
8388
+ dragMode = null;
8389
+ activePointerId = null;
8390
+ pointerDownInfo = null;
8391
+ scheduleDraw();
8392
+ return;
8393
+ }
7898
8394
  if (orderDragState) {
7899
8395
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7900
8396
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -8038,7 +8534,7 @@ function createChart(element, options = {}) {
8038
8534
  x: point.x,
8039
8535
  y: point.y
8040
8536
  };
8041
- if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
8537
+ if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
8042
8538
  drawingEditTextHandler?.(payload);
8043
8539
  } else {
8044
8540
  drawingDoubleClickHandler?.(payload);
@@ -8485,6 +8981,51 @@ function createChart(element, options = {}) {
8485
8981
  const onDrawingHover = (handler) => {
8486
8982
  drawingHoverHandler = handler;
8487
8983
  };
8984
+ const saveState = () => {
8985
+ const drawingDefaults = {};
8986
+ for (const [tool, defaults] of drawingToolDefaults) {
8987
+ drawingDefaults[tool] = { ...defaults };
8988
+ }
8989
+ return {
8990
+ version: 1,
8991
+ chartType: getChartType(),
8992
+ viewport: getViewport(),
8993
+ drawings: getDrawings(),
8994
+ indicators: getIndicators(),
8995
+ magnetMode: getMagnetMode(),
8996
+ drawingDefaults
8997
+ };
8998
+ };
8999
+ const loadState = (state) => {
9000
+ if (!state || typeof state !== "object") {
9001
+ return;
9002
+ }
9003
+ if (typeof state.chartType === "string") {
9004
+ setChartType(state.chartType);
9005
+ }
9006
+ if (Array.isArray(state.indicators)) {
9007
+ setIndicators(state.indicators);
9008
+ }
9009
+ if (Array.isArray(state.drawings)) {
9010
+ setDrawings(state.drawings);
9011
+ }
9012
+ if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
9013
+ setMagnetMode(state.magnetMode);
9014
+ }
9015
+ if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
9016
+ for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
9017
+ if (defaults && typeof defaults === "object") {
9018
+ setDrawingDefaults(tool, defaults);
9019
+ }
9020
+ }
9021
+ }
9022
+ if (state.viewport && typeof state.viewport === "object") {
9023
+ setViewport(state.viewport);
9024
+ }
9025
+ };
9026
+ const takeScreenshot = (options2 = {}) => {
9027
+ return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
9028
+ };
8488
9029
  const destroy = () => {
8489
9030
  cancelKineticPan();
8490
9031
  if (smoothingRafId !== null) {
@@ -8580,6 +9121,9 @@ function createChart(element, options = {}) {
8580
9121
  updateIndicator,
8581
9122
  removeIndicator,
8582
9123
  setIndicators,
9124
+ saveState,
9125
+ loadState,
9126
+ takeScreenshot,
8583
9127
  resize,
8584
9128
  destroy
8585
9129
  };