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.
package/dist/index.js CHANGED
@@ -4788,6 +4788,216 @@ function createChart(element, options = {}) {
4788
4788
  drawDrawingLabel(drawing.label, (leftX + rightX) / 2, topY - 4, drawing.color);
4789
4789
  }
4790
4790
  }
4791
+ } else if (drawing.type === "ellipse") {
4792
+ const p0 = drawing.points[0];
4793
+ const p1 = drawing.points[1];
4794
+ if (p0 && p1) {
4795
+ const px0 = xFromDrawingPoint(p0);
4796
+ const px1 = xFromDrawingPoint(p1);
4797
+ const y0 = yFromPrice(p0.price);
4798
+ const y1 = yFromPrice(p1.price);
4799
+ const cx = (px0 + px1) / 2;
4800
+ const cy = (y0 + y1) / 2;
4801
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
4802
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
4803
+ ctx.save();
4804
+ ctx.globalAlpha = draft ? 0.6 : 1;
4805
+ ctx.beginPath();
4806
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
4807
+ ctx.fillStyle = hexToRgba(drawing.color, 0.14);
4808
+ ctx.fill();
4809
+ ctx.lineWidth = Math.max(1, drawing.width);
4810
+ ctx.strokeStyle = drawing.color;
4811
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
4812
+ ctx.stroke();
4813
+ ctx.restore();
4814
+ handleAt(px0, y0, drawing.color);
4815
+ handleAt(px1, y1, drawing.color);
4816
+ handleAt(px0, y1, drawing.color);
4817
+ handleAt(px1, y0, drawing.color);
4818
+ if (drawing.label) {
4819
+ drawDrawingLabel(drawing.label, cx, cy - ry - 4, drawing.color);
4820
+ }
4821
+ }
4822
+ } else if (drawing.type === "parallel-channel") {
4823
+ const p0 = drawing.points[0];
4824
+ const p1 = drawing.points[1];
4825
+ const p2 = drawing.points[2];
4826
+ if (p0 && p1) {
4827
+ const x0 = xFromDrawingPoint(p0);
4828
+ const y0 = yFromPrice(p0.price);
4829
+ const x1 = xFromDrawingPoint(p1);
4830
+ const y1 = yFromPrice(p1.price);
4831
+ ctx.beginPath();
4832
+ ctx.moveTo(x0, y0);
4833
+ ctx.lineTo(x1, y1);
4834
+ ctx.stroke();
4835
+ handleAt(x0, y0, drawing.color);
4836
+ handleAt(x1, y1, drawing.color);
4837
+ if (p2) {
4838
+ const indexSpan = p1.index - p0.index;
4839
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
4840
+ const basePriceAtP2 = p0.price + slope * (p2.index - p0.index);
4841
+ const offset = p2.price - basePriceAtP2;
4842
+ const q0x = x0;
4843
+ const q0y = yFromPrice(p0.price + offset);
4844
+ const q1x = x1;
4845
+ const q1y = yFromPrice(p1.price + offset);
4846
+ ctx.save();
4847
+ ctx.globalAlpha = draft ? 0.5 : 1;
4848
+ ctx.fillStyle = hexToRgba(drawing.color, 0.08);
4849
+ ctx.beginPath();
4850
+ ctx.moveTo(x0, y0);
4851
+ ctx.lineTo(x1, y1);
4852
+ ctx.lineTo(q1x, q1y);
4853
+ ctx.lineTo(q0x, q0y);
4854
+ ctx.closePath();
4855
+ ctx.fill();
4856
+ ctx.restore();
4857
+ ctx.beginPath();
4858
+ ctx.moveTo(q0x, q0y);
4859
+ ctx.lineTo(q1x, q1y);
4860
+ ctx.stroke();
4861
+ ctx.save();
4862
+ ctx.setLineDash([4, 4]);
4863
+ ctx.globalAlpha = draft ? 0.5 : 0.7;
4864
+ ctx.beginPath();
4865
+ ctx.moveTo(x0, (y0 + q0y) / 2);
4866
+ ctx.lineTo(x1, (y1 + q1y) / 2);
4867
+ ctx.stroke();
4868
+ ctx.restore();
4869
+ handleAt(xFromDrawingPoint(p2), yFromPrice(p2.price), drawing.color);
4870
+ }
4871
+ if (drawing.label) {
4872
+ drawDrawingLabel(drawing.label, (x0 + x1) / 2, Math.min(y0, y1) - 4, drawing.color);
4873
+ }
4874
+ }
4875
+ } else if (drawing.type === "arrow") {
4876
+ const first = drawing.points[0];
4877
+ const second = drawing.points[1];
4878
+ if (first && second) {
4879
+ const x0 = xFromDrawingPoint(first);
4880
+ const y0 = yFromPrice(first.price);
4881
+ const x1 = xFromDrawingPoint(second);
4882
+ const y1 = yFromPrice(second.price);
4883
+ const angle = Math.atan2(y1 - y0, x1 - x0);
4884
+ const head = 5 + Math.max(1, drawing.width) * 3;
4885
+ const shaftX = x1 - Math.cos(angle) * head * 0.6;
4886
+ const shaftY = y1 - Math.sin(angle) * head * 0.6;
4887
+ ctx.beginPath();
4888
+ ctx.moveTo(x0, y0);
4889
+ ctx.lineTo(shaftX, shaftY);
4890
+ ctx.stroke();
4891
+ ctx.save();
4892
+ ctx.setLineDash([]);
4893
+ ctx.fillStyle = drawing.color;
4894
+ ctx.beginPath();
4895
+ ctx.moveTo(x1, y1);
4896
+ ctx.lineTo(x1 - Math.cos(angle - Math.PI / 7) * head, y1 - Math.sin(angle - Math.PI / 7) * head);
4897
+ ctx.lineTo(x1 - Math.cos(angle + Math.PI / 7) * head, y1 - Math.sin(angle + Math.PI / 7) * head);
4898
+ ctx.closePath();
4899
+ ctx.fill();
4900
+ ctx.restore();
4901
+ handleAt(x0, y0, drawing.color);
4902
+ handleAt(x1, y1, drawing.color);
4903
+ if (drawing.label) {
4904
+ const midX = (x0 + x1) / 2;
4905
+ const midY = (y0 + y1) / 2;
4906
+ drawDrawingLabel(drawing.label, midX, midY, drawing.color);
4907
+ }
4908
+ }
4909
+ } else if (drawing.type === "brush") {
4910
+ const pts = drawing.points;
4911
+ if (pts.length >= 2) {
4912
+ ctx.save();
4913
+ ctx.lineJoin = "round";
4914
+ ctx.lineCap = "round";
4915
+ ctx.beginPath();
4916
+ const startX = xFromDrawingPoint(pts[0]);
4917
+ const startY = yFromPrice(pts[0].price);
4918
+ ctx.moveTo(startX, startY);
4919
+ if (pts.length === 2) {
4920
+ ctx.lineTo(xFromDrawingPoint(pts[1]), yFromPrice(pts[1].price));
4921
+ } else {
4922
+ for (let i = 1; i < pts.length - 1; i += 1) {
4923
+ const cxp = xFromDrawingPoint(pts[i]);
4924
+ const cyp = yFromPrice(pts[i].price);
4925
+ const nx = xFromDrawingPoint(pts[i + 1]);
4926
+ const ny = yFromPrice(pts[i + 1].price);
4927
+ ctx.quadraticCurveTo(cxp, cyp, (cxp + nx) / 2, (cyp + ny) / 2);
4928
+ }
4929
+ const last = pts[pts.length - 1];
4930
+ ctx.lineTo(xFromDrawingPoint(last), yFromPrice(last.price));
4931
+ }
4932
+ ctx.stroke();
4933
+ ctx.restore();
4934
+ if (isSelected) {
4935
+ handleAt(startX, startY, drawing.color);
4936
+ const last = pts[pts.length - 1];
4937
+ handleAt(xFromDrawingPoint(last), yFromPrice(last.price), drawing.color);
4938
+ }
4939
+ }
4940
+ } else if (drawing.type === "callout") {
4941
+ const anchor = drawing.points[0];
4942
+ const boxPoint = drawing.points[1];
4943
+ if (anchor && boxPoint) {
4944
+ const ax = xFromDrawingPoint(anchor);
4945
+ const ay = yFromPrice(anchor.price);
4946
+ const bx = xFromDrawingPoint(boxPoint);
4947
+ const by = yFromPrice(boxPoint.price);
4948
+ const fontSize = Math.max(6, drawing.fontSize);
4949
+ const prevFont = ctx.font;
4950
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
4951
+ const rawLabel = drawing.label ?? "";
4952
+ const isPlaceholder = rawLabel.trim().length === 0;
4953
+ const lines = (isPlaceholder ? "Text" : rawLabel).split("\n");
4954
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
4955
+ const padX = 10;
4956
+ const padY = 7;
4957
+ const lineH = Math.round(fontSize * 1.35);
4958
+ const blockW = textW + padX * 2;
4959
+ const blockH = lines.length * lineH + padY * 2;
4960
+ const edgeX = Math.max(bx, Math.min(bx + blockW, ax));
4961
+ const edgeY = Math.max(by, Math.min(by + blockH, ay));
4962
+ const insideBox = ax >= bx && ax <= bx + blockW && ay >= by && ay <= by + blockH;
4963
+ if (!insideBox) {
4964
+ ctx.save();
4965
+ ctx.setLineDash([]);
4966
+ ctx.lineWidth = Math.max(1, drawing.width);
4967
+ ctx.beginPath();
4968
+ ctx.moveTo(edgeX, edgeY);
4969
+ ctx.lineTo(ax, ay);
4970
+ ctx.stroke();
4971
+ const angle = Math.atan2(ay - edgeY, ax - edgeX);
4972
+ const head = 7;
4973
+ ctx.fillStyle = drawing.color;
4974
+ ctx.beginPath();
4975
+ ctx.moveTo(ax, ay);
4976
+ ctx.lineTo(ax - Math.cos(angle - Math.PI / 7) * head, ay - Math.sin(angle - Math.PI / 7) * head);
4977
+ ctx.lineTo(ax - Math.cos(angle + Math.PI / 7) * head, ay - Math.sin(angle + Math.PI / 7) * head);
4978
+ ctx.closePath();
4979
+ ctx.fill();
4980
+ ctx.restore();
4981
+ }
4982
+ ctx.save();
4983
+ ctx.setLineDash([]);
4984
+ ctx.fillStyle = hexToRgba(drawing.color, 0.16);
4985
+ fillRoundedRect(bx, by, blockW, blockH, 6);
4986
+ ctx.strokeStyle = drawing.color;
4987
+ ctx.lineWidth = Math.max(1, drawing.width);
4988
+ strokeRoundedRect(bx, by, blockW, blockH, 6);
4989
+ ctx.fillStyle = drawing.color;
4990
+ ctx.globalAlpha = (draft ? 0.72 : 1) * (isPlaceholder ? 0.55 : 1);
4991
+ ctx.textAlign = "left";
4992
+ ctx.textBaseline = "middle";
4993
+ lines.forEach((line, lineIndex) => {
4994
+ ctx.fillText(line, bx + padX, by + padY + lineIndex * lineH + lineH / 2);
4995
+ });
4996
+ ctx.restore();
4997
+ ctx.font = prevFont;
4998
+ handleAt(ax, ay, drawing.color);
4999
+ handleAt(bx, by, drawing.color);
5000
+ }
4791
5001
  } else if (drawing.type === "price-range") {
4792
5002
  const p0 = drawing.points[0];
4793
5003
  const p1 = drawing.points[1];
@@ -6842,6 +7052,126 @@ function createChart(element, options = {}) {
6842
7052
  if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
6843
7053
  return { drawing, target: "line" };
6844
7054
  }
7055
+ } else if (drawing.type === "ellipse") {
7056
+ const p0 = drawing.points[0];
7057
+ const p1 = drawing.points[1];
7058
+ if (!p0 || !p1) continue;
7059
+ const px0 = canvasXFromDrawingPoint(p0);
7060
+ const px1 = canvasXFromDrawingPoint(p1);
7061
+ const y0 = canvasYFromDrawingPrice(p0.price);
7062
+ const y1 = canvasYFromDrawingPrice(p1.price);
7063
+ if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7064
+ if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7065
+ if (Math.hypot(x - px0, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 2 };
7066
+ if (Math.hypot(x - px1, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 3 };
7067
+ const cx = (px0 + px1) / 2;
7068
+ const cy = (y0 + y1) / 2;
7069
+ const rx = Math.max(1, Math.abs(px1 - px0) / 2);
7070
+ const ry = Math.max(1, Math.abs(y1 - y0) / 2);
7071
+ const norm = ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;
7072
+ if (norm <= 1) {
7073
+ return { drawing, target: "line" };
7074
+ }
7075
+ } else if (drawing.type === "parallel-channel") {
7076
+ const p0 = drawing.points[0];
7077
+ const p1 = drawing.points[1];
7078
+ const p2 = drawing.points[2];
7079
+ if (!p0 || !p1) continue;
7080
+ const x0 = canvasXFromDrawingPoint(p0);
7081
+ const y0 = canvasYFromDrawingPrice(p0.price);
7082
+ const x1 = canvasXFromDrawingPoint(p1);
7083
+ const y1 = canvasYFromDrawingPrice(p1.price);
7084
+ if (Math.hypot(x - x0, y - y0) <= 8) return { drawing, target: "handle", pointIndex: 0 };
7085
+ if (Math.hypot(x - x1, y - y1) <= 8) return { drawing, target: "handle", pointIndex: 1 };
7086
+ if (p2) {
7087
+ const x2 = canvasXFromDrawingPoint(p2);
7088
+ const y2 = canvasYFromDrawingPrice(p2.price);
7089
+ if (Math.hypot(x - x2, y - y2) <= 8) return { drawing, target: "handle", pointIndex: 2 };
7090
+ const indexSpan = p1.index - p0.index;
7091
+ const slope = indexSpan !== 0 ? (p1.price - p0.price) / indexSpan : 0;
7092
+ const offset = p2.price - (p0.price + slope * (p2.index - p0.index));
7093
+ const q0y = canvasYFromDrawingPrice(p0.price + offset);
7094
+ const q1y = canvasYFromDrawingPrice(p1.price + offset);
7095
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x0, q0y, x1, q1y) <= 6) {
7096
+ return { drawing, target: "line" };
7097
+ }
7098
+ const minX = Math.min(x0, x1);
7099
+ const maxX = Math.max(x0, x1);
7100
+ if (x >= minX && x <= maxX && maxX > minX) {
7101
+ const t = (x - x0) / (x1 - x0);
7102
+ const baseY = y0 + t * (y1 - y0);
7103
+ const parY = q0y + t * (q1y - q0y);
7104
+ if (y >= Math.min(baseY, parY) && y <= Math.max(baseY, parY)) {
7105
+ return { drawing, target: "line" };
7106
+ }
7107
+ }
7108
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
7109
+ return { drawing, target: "line" };
7110
+ }
7111
+ } else if (drawing.type === "arrow") {
7112
+ const first = drawing.points[0];
7113
+ const second = drawing.points[1];
7114
+ if (!first || !second) continue;
7115
+ const x1 = canvasXFromDrawingPoint(first);
7116
+ const y1 = canvasYFromDrawingPrice(first.price);
7117
+ const x2 = canvasXFromDrawingPoint(second);
7118
+ const y2 = canvasYFromDrawingPrice(second.price);
7119
+ if (Math.hypot(x - x1, y - y1) <= 8) {
7120
+ return { drawing, target: "handle", pointIndex: 0 };
7121
+ }
7122
+ if (Math.hypot(x - x2, y - y2) <= 8) {
7123
+ return { drawing, target: "handle", pointIndex: 1 };
7124
+ }
7125
+ if (distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
7126
+ return { drawing, target: "line" };
7127
+ }
7128
+ } else if (drawing.type === "brush") {
7129
+ const pts = drawing.points;
7130
+ if (pts.length < 2) continue;
7131
+ let hitBody = false;
7132
+ let prevX = canvasXFromDrawingPoint(pts[0]);
7133
+ let prevY = canvasYFromDrawingPrice(pts[0].price);
7134
+ for (let i = 1; i < pts.length; i += 1) {
7135
+ const nextX = canvasXFromDrawingPoint(pts[i]);
7136
+ const nextY = canvasYFromDrawingPrice(pts[i].price);
7137
+ if (distanceToSegment(x, y, prevX, prevY, nextX, nextY) <= 7) {
7138
+ hitBody = true;
7139
+ break;
7140
+ }
7141
+ prevX = nextX;
7142
+ prevY = nextY;
7143
+ }
7144
+ if (hitBody) {
7145
+ return { drawing, target: "line" };
7146
+ }
7147
+ } else if (drawing.type === "callout") {
7148
+ const anchor = drawing.points[0];
7149
+ const boxPoint = drawing.points[1];
7150
+ if (!anchor || !boxPoint) continue;
7151
+ const ax = canvasXFromDrawingPoint(anchor);
7152
+ const ay = canvasYFromDrawingPrice(anchor.price);
7153
+ const bx = canvasXFromDrawingPoint(boxPoint);
7154
+ const by = canvasYFromDrawingPrice(boxPoint.price);
7155
+ if (Math.hypot(x - ax, y - ay) <= 9) return { drawing, target: "handle", pointIndex: 0 };
7156
+ if (Math.hypot(x - bx, y - by) <= 9) return { drawing, target: "handle", pointIndex: 1 };
7157
+ const fontSize = Math.max(6, drawing.fontSize);
7158
+ const prevFont = ctx.font;
7159
+ ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
7160
+ const rawLabel = drawing.label ?? "";
7161
+ const lines = (rawLabel.trim().length === 0 ? "Text" : rawLabel).split("\n");
7162
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
7163
+ ctx.font = prevFont;
7164
+ const padX = 10;
7165
+ const padY = 7;
7166
+ const lineH = Math.round(fontSize * 1.35);
7167
+ const blockW = textW + padX * 2;
7168
+ const blockH = lines.length * lineH + padY * 2;
7169
+ if (x >= bx && x <= bx + blockW && y >= by && y <= by + blockH) {
7170
+ return { drawing, target: "line" };
7171
+ }
7172
+ if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5) {
7173
+ return { drawing, target: "line" };
7174
+ }
6845
7175
  } else if (drawing.type === "price-range") {
6846
7176
  const p0 = drawing.points[0];
6847
7177
  const p1 = drawing.points[1];
@@ -7070,6 +7400,128 @@ function createChart(element, options = {}) {
7070
7400
  scheduleDraw();
7071
7401
  return true;
7072
7402
  }
7403
+ if (activeDrawingTool === "arrow") {
7404
+ if (draftDrawing?.type === "arrow") {
7405
+ const anchor = draftDrawing.points[0];
7406
+ const endPoint = shiftKeyActive ? constrainAngleFromAnchor(anchor, x, y) ?? point : point;
7407
+ const completed = normalizeDrawingState({
7408
+ ...serializeDrawing(draftDrawing),
7409
+ points: [anchor, endPoint]
7410
+ });
7411
+ drawings.push(completed);
7412
+ draftDrawing = null;
7413
+ activeDrawingTool = null;
7414
+ emitDrawingsChange();
7415
+ scheduleDraw();
7416
+ return true;
7417
+ }
7418
+ const defaults = getDrawingToolDefaults("arrow");
7419
+ draftDrawing = normalizeDrawingState({
7420
+ type: "arrow",
7421
+ points: [point, point],
7422
+ color: defaults.color ?? "#2563eb",
7423
+ style: defaults.style ?? "solid",
7424
+ width: defaults.width ?? 2
7425
+ });
7426
+ scheduleDraw();
7427
+ return true;
7428
+ }
7429
+ if (activeDrawingTool === "parallel-channel") {
7430
+ if (draftDrawing?.type === "parallel-channel") {
7431
+ if (draftDrawing.points.length < 3) {
7432
+ draftDrawing = normalizeDrawingState({
7433
+ ...serializeDrawing(draftDrawing),
7434
+ points: [...draftDrawing.points.slice(0, -1), point, point]
7435
+ });
7436
+ scheduleDraw();
7437
+ return true;
7438
+ }
7439
+ const completed = normalizeDrawingState({
7440
+ ...serializeDrawing(draftDrawing),
7441
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
7442
+ });
7443
+ drawings.push(completed);
7444
+ draftDrawing = null;
7445
+ activeDrawingTool = null;
7446
+ emitDrawingsChange();
7447
+ scheduleDraw();
7448
+ return true;
7449
+ }
7450
+ const defaults = getDrawingToolDefaults("parallel-channel");
7451
+ draftDrawing = normalizeDrawingState({
7452
+ type: "parallel-channel",
7453
+ points: [point, point],
7454
+ color: defaults.color ?? "#2563eb",
7455
+ style: defaults.style ?? "solid",
7456
+ width: defaults.width ?? 2
7457
+ });
7458
+ scheduleDraw();
7459
+ return true;
7460
+ }
7461
+ if (activeDrawingTool === "ellipse") {
7462
+ if (draftDrawing?.type === "ellipse") {
7463
+ const completed = normalizeDrawingState({
7464
+ ...serializeDrawing(draftDrawing),
7465
+ points: [draftDrawing.points[0], point]
7466
+ });
7467
+ drawings.push(completed);
7468
+ draftDrawing = null;
7469
+ activeDrawingTool = null;
7470
+ emitDrawingsChange();
7471
+ scheduleDraw();
7472
+ return true;
7473
+ }
7474
+ const defaults = getDrawingToolDefaults("ellipse");
7475
+ draftDrawing = normalizeDrawingState({
7476
+ type: "ellipse",
7477
+ points: [point, point],
7478
+ color: defaults.color ?? "#2962ff",
7479
+ style: defaults.style ?? "solid",
7480
+ width: defaults.width ?? 1
7481
+ });
7482
+ scheduleDraw();
7483
+ return true;
7484
+ }
7485
+ if (activeDrawingTool === "callout") {
7486
+ if (draftDrawing?.type === "callout") {
7487
+ const completed = normalizeDrawingState({
7488
+ ...serializeDrawing(draftDrawing),
7489
+ points: [draftDrawing.points[0], point]
7490
+ });
7491
+ drawings.push(completed);
7492
+ draftDrawing = null;
7493
+ activeDrawingTool = null;
7494
+ selectedDrawingId = completed.id;
7495
+ emitDrawingsChange();
7496
+ scheduleDraw();
7497
+ drawingEditTextHandler?.({ drawing: serializeDrawing(completed), target: "line", x, y });
7498
+ return true;
7499
+ }
7500
+ const defaults = getDrawingToolDefaults("callout");
7501
+ draftDrawing = normalizeDrawingState({
7502
+ type: "callout",
7503
+ points: [point, point],
7504
+ color: defaults.color ?? "#2962ff",
7505
+ style: defaults.style ?? "solid",
7506
+ width: defaults.width ?? 1,
7507
+ ...defaults.fontSize === void 0 ? {} : { fontSize: defaults.fontSize },
7508
+ label: ""
7509
+ });
7510
+ scheduleDraw();
7511
+ return true;
7512
+ }
7513
+ if (activeDrawingTool === "brush") {
7514
+ const defaults = getDrawingToolDefaults("brush");
7515
+ draftDrawing = normalizeDrawingState({
7516
+ type: "brush",
7517
+ points: [point],
7518
+ color: defaults.color ?? "#f59e0b",
7519
+ style: defaults.style ?? "solid",
7520
+ width: defaults.width ?? 2
7521
+ });
7522
+ scheduleDraw();
7523
+ return true;
7524
+ }
7073
7525
  if (activeDrawingTool === "fib-retracement") {
7074
7526
  if (draftDrawing?.type === "fib-retracement") {
7075
7527
  const completed = normalizeDrawingState({
@@ -7273,7 +7725,7 @@ function createChart(element, options = {}) {
7273
7725
  }
7274
7726
  return { ...drawing, points: pts };
7275
7727
  }
7276
- if (drawingDragState.target === "handle" && drawing.type === "rectangle") {
7728
+ if (drawingDragState.target === "handle" && (drawing.type === "rectangle" || drawing.type === "ellipse")) {
7277
7729
  const pts = drawing.points.map((point) => ({ ...point }));
7278
7730
  const p0 = pts[0];
7279
7731
  const p1 = pts[1];
@@ -7292,7 +7744,7 @@ function createChart(element, options = {}) {
7292
7744
  }
7293
7745
  return { ...drawing, points: pts };
7294
7746
  }
7295
- if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray")) {
7747
+ if (drawingDragState.target === "handle" && shiftKeyActive && (drawing.type === "trendline" || drawing.type === "ray" || drawing.type === "arrow")) {
7296
7748
  const pointIndex = drawingDragState.pointIndex ?? 0;
7297
7749
  const anchor = drawing.points[pointIndex === 0 ? 1 : 0];
7298
7750
  const constrained = anchor ? constrainAngleFromAnchor(anchor, x, y) : null;
@@ -7542,6 +7994,10 @@ function createChart(element, options = {}) {
7542
7994
  if (region === "plot" && handleDrawingToolPointerDown(point.x, point.y)) {
7543
7995
  setCrosshairPoint(null);
7544
7996
  canvas.style.cursor = "crosshair";
7997
+ if (activeDrawingTool === "brush" && draftDrawing?.type === "brush") {
7998
+ activePointerId = event.pointerId;
7999
+ capturePointer(event.pointerId);
8000
+ }
7545
8001
  return;
7546
8002
  }
7547
8003
  isDragging = true;
@@ -7626,9 +8082,30 @@ function createChart(element, options = {}) {
7626
8082
  setCrosshairPoint(null);
7627
8083
  return;
7628
8084
  }
7629
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "rectangle" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
8085
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8086
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
8087
+ return;
8088
+ }
8089
+ const nextPoint = drawingPointFromCanvas(point.x, point.y);
8090
+ if (nextPoint) {
8091
+ const last = draftDrawing.points[draftDrawing.points.length - 1];
8092
+ const lastX = last ? canvasXFromDrawingPoint(last) : Number.NEGATIVE_INFINITY;
8093
+ const lastY = last ? canvasYFromDrawingPrice(last.price) : Number.NEGATIVE_INFINITY;
8094
+ if (Math.hypot(point.x - lastX, point.y - lastY) >= 3) {
8095
+ draftDrawing = {
8096
+ ...draftDrawing,
8097
+ points: [...draftDrawing.points, nextPoint]
8098
+ };
8099
+ }
8100
+ canvas.style.cursor = "crosshair";
8101
+ setCrosshairPoint(null);
8102
+ scheduleDraw();
8103
+ }
8104
+ return;
8105
+ }
8106
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "arrow" || activeDrawingTool === "rectangle" || activeDrawingTool === "ellipse" || activeDrawingTool === "callout" || activeDrawingTool === "parallel-channel" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
7630
8107
  let nextPoint = drawingPointFromCanvas(point.x, point.y);
7631
- if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray") && draftDrawing.points[0]) {
8108
+ if (nextPoint && shiftKeyActive && (draftDrawing.type === "trendline" || draftDrawing.type === "ray" || draftDrawing.type === "arrow") && draftDrawing.points[0]) {
7632
8109
  nextPoint = constrainAngleFromAnchor(draftDrawing.points[0], point.x, point.y) ?? nextPoint;
7633
8110
  }
7634
8111
  if (nextPoint) {
@@ -7865,6 +8342,25 @@ function createChart(element, options = {}) {
7865
8342
  scheduleDraw();
7866
8343
  return;
7867
8344
  }
8345
+ if (draftDrawing?.type === "brush" && activeDrawingTool === "brush") {
8346
+ if (draftDrawing.points.length >= 2) {
8347
+ const completed = normalizeDrawingState(serializeDrawing(draftDrawing));
8348
+ drawings.push(completed);
8349
+ draftDrawing = null;
8350
+ activeDrawingTool = null;
8351
+ canvas.style.cursor = "default";
8352
+ emitDrawingsChange();
8353
+ } else {
8354
+ draftDrawing = null;
8355
+ canvas.style.cursor = "crosshair";
8356
+ }
8357
+ isDragging = false;
8358
+ dragMode = null;
8359
+ activePointerId = null;
8360
+ pointerDownInfo = null;
8361
+ scheduleDraw();
8362
+ return;
8363
+ }
7868
8364
  if (orderDragState) {
7869
8365
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7870
8366
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -8008,7 +8504,7 @@ function createChart(element, options = {}) {
8008
8504
  x: point.x,
8009
8505
  y: point.y
8010
8506
  };
8011
- if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note") {
8507
+ if (drawingHit.drawing.type === "text" || drawingHit.drawing.type === "note" || drawingHit.drawing.type === "callout") {
8012
8508
  drawingEditTextHandler?.(payload);
8013
8509
  } else {
8014
8510
  drawingDoubleClickHandler?.(payload);
@@ -8455,6 +8951,51 @@ function createChart(element, options = {}) {
8455
8951
  const onDrawingHover = (handler) => {
8456
8952
  drawingHoverHandler = handler;
8457
8953
  };
8954
+ const saveState = () => {
8955
+ const drawingDefaults = {};
8956
+ for (const [tool, defaults] of drawingToolDefaults) {
8957
+ drawingDefaults[tool] = { ...defaults };
8958
+ }
8959
+ return {
8960
+ version: 1,
8961
+ chartType: getChartType(),
8962
+ viewport: getViewport(),
8963
+ drawings: getDrawings(),
8964
+ indicators: getIndicators(),
8965
+ magnetMode: getMagnetMode(),
8966
+ drawingDefaults
8967
+ };
8968
+ };
8969
+ const loadState = (state) => {
8970
+ if (!state || typeof state !== "object") {
8971
+ return;
8972
+ }
8973
+ if (typeof state.chartType === "string") {
8974
+ setChartType(state.chartType);
8975
+ }
8976
+ if (Array.isArray(state.indicators)) {
8977
+ setIndicators(state.indicators);
8978
+ }
8979
+ if (Array.isArray(state.drawings)) {
8980
+ setDrawings(state.drawings);
8981
+ }
8982
+ if (state.magnetMode === "none" || state.magnetMode === "weak" || state.magnetMode === "strong") {
8983
+ setMagnetMode(state.magnetMode);
8984
+ }
8985
+ if (state.drawingDefaults && typeof state.drawingDefaults === "object") {
8986
+ for (const [tool, defaults] of Object.entries(state.drawingDefaults)) {
8987
+ if (defaults && typeof defaults === "object") {
8988
+ setDrawingDefaults(tool, defaults);
8989
+ }
8990
+ }
8991
+ }
8992
+ if (state.viewport && typeof state.viewport === "object") {
8993
+ setViewport(state.viewport);
8994
+ }
8995
+ };
8996
+ const takeScreenshot = (options2 = {}) => {
8997
+ return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
8998
+ };
8458
8999
  const destroy = () => {
8459
9000
  cancelKineticPan();
8460
9001
  if (smoothingRafId !== null) {
@@ -8550,6 +9091,9 @@ function createChart(element, options = {}) {
8550
9091
  updateIndicator,
8551
9092
  removeIndicator,
8552
9093
  setIndicators,
9094
+ saveState,
9095
+ loadState,
9096
+ takeScreenshot,
8553
9097
  resize,
8554
9098
  destroy
8555
9099
  };