traderead-charts 1.2.0 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 — 2026-09-19
4
+
5
+ - **Measure tool** (ruler, in the drawing toolbar): drag between two points
6
+ and the box reports the move three ways at once — absolute price change,
7
+ percentage, and the span in bars and in time (`+563.22 (+0.85%)` /
8
+ `39 bars · 1d 14h`). Green up, red down, with an arrow following the drag,
9
+ so the sign reads from the direction you dragged rather than from the
10
+ chart. Selectable, movable and deletable like any other drawing, and it
11
+ works with touch.
12
+
3
13
  ## 1.2.0 — 2026-09-13
4
14
 
5
15
  - **In-widget timeframe picker** (`timeframes: {list, active, onSelect}`):
package/README.md CHANGED
@@ -58,7 +58,7 @@ npm install traderead-charts
58
58
  or straight from a CDN, no build step:
59
59
 
60
60
  ```html
61
- <script src="https://cdn.jsdelivr.net/npm/traderead-charts@1.1.0/src/traderead-charts.js"></script>
61
+ <script src="https://cdn.jsdelivr.net/npm/traderead-charts@1.2.0/src/traderead-charts.js"></script>
62
62
  ```
63
63
 
64
64
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderead-charts",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Complete financial charting widget — candlesticks, indicators with a pro-style panel, drawing tools, symbol search, themes. Batteries included, zero dependencies, in production on traderead.ai.",
5
5
  "main": "src/traderead-charts.js",
6
6
  "files": [
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * TradeRead Charts v1.2.0
2
+ * TradeRead Charts v1.3.0
3
3
  * Copyright (c) 2026 Marcel Todea / TradeRead — traderead.ai
4
4
  * Original work, written from first principles.
5
5
  * TradeRead Community License 1.0 (see LICENSE.md): free to use, including
@@ -203,6 +203,7 @@
203
203
  cursor: SVGI('<path d="M4 2 L12 8.5 L8.2 9.3 L6.2 13.5 Z" fill="currentColor" stroke="none"/>'),
204
204
  trend: SVGI('<line x1="3" y1="13" x2="13" y2="3"/><circle cx="3" cy="13" r="1.4" fill="currentColor" stroke="none"/><circle cx="13" cy="3" r="1.4" fill="currentColor" stroke="none"/>'),
205
205
  hline: SVGI('<line x1="2" y1="8" x2="14" y2="8"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/>'),
206
+ measure: SVGI('<rect x="2.5" y="5.5" width="11" height="5" rx="1"/><line x1="5" y1="5.5" x2="5" y2="8"/><line x1="8" y1="5.5" x2="8" y2="9"/><line x1="11" y1="5.5" x2="11" y2="8"/>'),
206
207
  rect: SVGI('<rect x="3" y="4.5" width="10" height="7" rx="1"/>'),
207
208
  fib: SVGI('<line x1="2.5" y1="4" x2="13.5" y2="4"/><line x1="2.5" y1="8" x2="13.5" y2="8" opacity="0.55"/><line x1="2.5" y1="12" x2="13.5" y2="12"/>'),
208
209
  text: SVGI('<path d="M4 4 H12 M8 4 V12.5" />'),
@@ -255,7 +256,8 @@
255
256
  }
256
257
  var toolBtns = {};
257
258
  [[ICONS.cursor, "Select / move", null], [ICONS.trend, "Trend line", "trend"], [ICONS.hline, "Horizontal line", "hline"],
258
- [ICONS.rect, "Rectangle", "rect"], [ICONS.fib, "Fibonacci retracement", "fib"], [ICONS.text, "Text", "text"]].forEach(function (t) {
259
+ [ICONS.rect, "Rectangle", "rect"], [ICONS.fib, "Fibonacci retracement", "fib"], [ICONS.text, "Text", "text"],
260
+ [ICONS.measure, "Measure \u2014 price move, % and span", "measure"]].forEach(function (t) {
259
261
  toolBtns[t[2] || "cursor"] = btn(t[0], t[1], function (b) { self.setTool(t[2]); mark(t[2] ? b : toolBtns.cursor); });
260
262
  });
261
263
  mark(toolBtns.cursor);
@@ -1405,6 +1407,61 @@
1405
1407
  } else if (d.type === "trend") {
1406
1408
  c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.stroke();
1407
1409
  if (sel) { c.fillRect(x1 - 3, y1 - 3, 6, 6); c.fillRect(x2 - 3, y2 - 3, 6, 6); }
1410
+ } else if (d.type === "measure") {
1411
+ // A ruler answers three questions at once — how far, how far in
1412
+ // percent, and over how long — so all three are on the box. The
1413
+ // percentage is measured from the FIRST point: dragging from the low
1414
+ // to the high is a rise, and the sign follows the drag, not the chart.
1415
+ var mUp = d.p2 >= d.p1;
1416
+ var mCol = mUp ? (this.opt.upColor || "#22c55e") : (this.opt.downColor || "#ef4444");
1417
+ var mx = Math.min(x1, x2), my = Math.min(y1, y2);
1418
+ var mw = Math.abs(x2 - x1), mh = Math.abs(y2 - y1);
1419
+ c.save();
1420
+ c.fillStyle = mUp ? "rgba(34,197,94,0.13)" : "rgba(239,68,68,0.13)";
1421
+ c.fillRect(mx, my, mw, mh);
1422
+ c.strokeStyle = mCol; c.lineWidth = 1;
1423
+ c.strokeRect(Math.round(mx) + 0.5, Math.round(my) + 0.5, Math.round(mw), Math.round(mh));
1424
+ // vertical arrow through the middle, pointing the way the move went
1425
+ var amx = mx + mw / 2;
1426
+ c.beginPath(); c.moveTo(amx, y1); c.lineTo(amx, y2); c.stroke();
1427
+ var ah = y2 >= y1 ? -5 : 5;
1428
+ c.beginPath(); c.moveTo(amx, y2); c.lineTo(amx - 4, y2 + ah); c.lineTo(amx + 4, y2 + ah);
1429
+ c.closePath(); c.fillStyle = mCol; c.fill();
1430
+
1431
+ var dPrice = d.p2 - d.p1;
1432
+ var dPct = d.p1 ? (dPrice / Math.abs(d.p1)) * 100 : 0;
1433
+ var digits = Math.abs(d.p1) >= 1000 ? 2 : (Math.abs(d.p1) >= 1 ? 2 : 5);
1434
+ var nBars = Math.abs(Math.round(this.xToIndex(x2) - this.xToIndex(x1)));
1435
+ var secs = Math.abs((d.t2 || 0) - (d.t1 || 0));
1436
+ var span = "";
1437
+ if (secs >= 86400) span = Math.floor(secs / 86400) + "d " + Math.floor((secs % 86400) / 3600) + "h";
1438
+ else if (secs >= 3600) span = Math.floor(secs / 3600) + "h " + Math.floor((secs % 3600) / 60) + "m";
1439
+ else span = Math.max(1, Math.round(secs / 60)) + "m";
1440
+ var sign = dPrice >= 0 ? "+" : "\u2212";
1441
+ var l1 = sign + fmtPrice(Math.abs(dPrice), digits) + " (" + sign + Math.abs(dPct).toFixed(2) + "%)";
1442
+ var l2 = nBars + (nBars === 1 ? " bar" : " bars") + " \u00b7 " + span;
1443
+
1444
+ c.font = "700 11px -apple-system, 'Segoe UI', sans-serif";
1445
+ var w1 = c.measureText(l1).width;
1446
+ c.font = "600 10px -apple-system, 'Segoe UI', sans-serif";
1447
+ var w2 = c.measureText(l2).width;
1448
+ var bw = Math.max(w1, w2) + 16, bh = 32;
1449
+ // sit above the end point, and flip below when there is no room
1450
+ var bx = clamp(amx - bw / 2, 2, Math.max(2, this._plotW() - bw - 2));
1451
+ var by = (y2 <= y1) ? y2 - bh - 6 : y2 + 6;
1452
+ if (by < pp.y0 + 2) by = y2 + 6;
1453
+ if (by + bh > pp.y0 + pp.h - 2) by = Math.max(pp.y0 + 2, y2 - bh - 6);
1454
+ c.fillStyle = mCol;
1455
+ if (c.roundRect) { c.beginPath(); c.roundRect(bx, by, bw, bh, 5); c.fill(); }
1456
+ else c.fillRect(bx, by, bw, bh);
1457
+ c.fillStyle = "#fff";
1458
+ c.font = "700 11px -apple-system, 'Segoe UI', sans-serif";
1459
+ c.fillText(l1, bx + 8, by + 14);
1460
+ c.font = "600 10px -apple-system, 'Segoe UI', sans-serif";
1461
+ c.globalAlpha = 0.85;
1462
+ c.fillText(l2, bx + 8, by + 26);
1463
+ c.restore();
1464
+ if (sel) { c.fillStyle = mCol; c.fillRect(x1 - 3, y1 - 3, 6, 6); c.fillRect(x2 - 3, y2 - 3, 6, 6); }
1408
1465
  } else if (d.type === "rect") {
1409
1466
  c.save(); c.globalAlpha = 0.12;
1410
1467
  c.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
@@ -1455,7 +1512,7 @@
1455
1512
  var x1 = isNum(d.t1) ? this.timeToX(d.t1) : null, y1 = isNum(d.p1) ? pp.toY(d.p1) : null;
1456
1513
  var x2 = isNum(d.t2) ? this.timeToX(d.t2) : null, y2 = isNum(d.p2) ? pp.toY(d.p2) : null;
1457
1514
  if (d.id === this._selected && x2 !== null && Math.abs(x - x2) < 7 && Math.abs(y - y2) < 7 &&
1458
- (d.type === "rect" || d.type === "fib" || d.type === "trend")) return { d: d, handle: "p2" };
1515
+ (d.type === "rect" || d.type === "fib" || d.type === "trend" || d.type === "measure")) return { d: d, handle: "p2" };
1459
1516
  if (d.id === this._selected && d.type === "text") {
1460
1517
  var fsz = d.size || 12, tw2 = d._tw || (d.text || "").length * fsz * 0.62;
1461
1518
  if (Math.abs(x - (x1 + tw2 + 6)) < 8 && Math.abs(y - y1) < 8) return { d: d, handle: "size" };
@@ -1463,7 +1520,7 @@
1463
1520
  if (d.id === this._selected && d.type === "trend" && Math.abs(x - x1) < 7 && Math.abs(y - y1) < 7) return { d: d, handle: "p1" };
1464
1521
  if (d.type === "hline") { if (Math.abs(y - y1) < 6) return { d: d, handle: null }; }
1465
1522
  else if (d.type === "trend") { if (distToSeg(x, y, { x: x1, y: y1 }, { x: x2, y: y2 }) < 6) return { d: d, handle: null }; }
1466
- else if (d.type === "rect" || d.type === "fib") {
1523
+ else if (d.type === "rect" || d.type === "fib" || d.type === "measure") {
1467
1524
  if (x >= Math.min(x1, x2) - 4 && x <= Math.max(x1, x2) + 4 &&
1468
1525
  y >= Math.min(y1, y2) - 4 && y <= Math.max(y1, y2) + 4) return { d: d, handle: null };
1469
1526
  } else if (d.type === "text") {