traderead-charts 1.1.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,22 @@
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
+
13
+ ## 1.2.0 — 2026-09-13
14
+
15
+ - **In-widget timeframe picker** (`timeframes: {list, active, onSelect}`):
16
+ the host page's own TF buttons disappear under a fullscreen widget —
17
+ the pill in the header opens a grid menu that rides inside the frame,
18
+ works in fullscreen, closes on outside tap.
19
+
3
20
  ## 1.1.0 — 2026-09-13
4
21
 
5
22
  First npm release (`npm install traderead-charts`). Since 1.0.0:
package/README.md CHANGED
@@ -49,6 +49,18 @@ params, all persisted per key), a chart-type menu (seven types,
49
49
  Heikin Ashi included) and a PNG snapshot that opens a preview with explicit Download / Copy actions — nothing downloads by itself. Hosts that want only the
50
50
  engine pass `ui: false` and drive the same APIs themselves.
51
51
 
52
+ ## Install
53
+
54
+ ```
55
+ npm install traderead-charts
56
+ ```
57
+
58
+ or straight from a CDN, no build step:
59
+
60
+ ```html
61
+ <script src="https://cdn.jsdelivr.net/npm/traderead-charts@1.2.0/src/traderead-charts.js"></script>
62
+ ```
63
+
52
64
  ## Quick start
53
65
 
54
66
  ```html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderead-charts",
3
- "version": "1.1.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.0.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
@@ -35,6 +35,11 @@
35
35
  minPricePaneFrac: 0.45, // price pane never shrinks below this share
36
36
  onFullscreenToggle: null, // host override: an iframe cannot fullscreen itself on iOS
37
37
  logoHref: null, // override the badge link (default carries utm_source=widget)
38
+ // {list:[{label,value}...], active, onSelect(value)} — an in-widget
39
+ // timeframe picker. The host page's own TF buttons disappear under a
40
+ // fullscreen widget (owner's find, 12 Sep 2026); inside the frame the
41
+ // picker survives every mode.
42
+ timeframes: null,
38
43
  logo: true,
39
44
  ui: true,
40
45
  maxIndicators: 0, // 0 = unlimited (Pro); Standard builds pass 4 // the bundled rail + indicator panel + type switcher + camera
@@ -198,6 +203,7 @@
198
203
  cursor: SVGI('<path d="M4 2 L12 8.5 L8.2 9.3 L6.2 13.5 Z" fill="currentColor" stroke="none"/>'),
199
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"/>'),
200
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"/>'),
201
207
  rect: SVGI('<rect x="3" y="4.5" width="10" height="7" rx="1"/>'),
202
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"/>'),
203
209
  text: SVGI('<path d="M4 4 H12 M8 4 V12.5" />'),
@@ -250,7 +256,8 @@
250
256
  }
251
257
  var toolBtns = {};
252
258
  [[ICONS.cursor, "Select / move", null], [ICONS.trend, "Trend line", "trend"], [ICONS.hline, "Horizontal line", "hline"],
253
- [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) {
254
261
  toolBtns[t[2] || "cursor"] = btn(t[0], t[1], function (b) { self.setTool(t[2]); mark(t[2] ? b : toolBtns.cursor); });
255
262
  });
256
263
  mark(toolBtns.cursor);
@@ -1015,14 +1022,21 @@
1015
1022
  var h = document.createElement("div");
1016
1023
  h.className = "trc-header";
1017
1024
  var clickable = !!(this.opt.symbolSearch && this.opt.symbolSearch.search);
1025
+ var hasTf = !!(this.opt.timeframes && this.opt.timeframes.list && this.opt.timeframes.list.length);
1018
1026
  h.style.cssText = "position:absolute;left:" + (this.opt.ui ? 56 : 12) + "px;top:10px;z-index:4;display:flex;align-items:baseline;gap:8px;" +
1019
1027
  "font:800 15px -apple-system,'Segoe UI',sans-serif;user-select:none;" +
1020
- (clickable ? "cursor:pointer;" : "pointer-events:none;");
1021
- if (clickable) {
1022
- var self2 = this;
1023
- h.title = "Search ticker or name";
1024
- h.addEventListener("click", function () { self2._toggleSearch(); });
1025
- }
1028
+ (clickable ? "cursor:pointer;" : (hasTf ? "" : "pointer-events:none;"));
1029
+ var self2 = this;
1030
+ if (clickable) h.title = "Search ticker or name";
1031
+ // delegare: pastila de TF se reconstruieste la fiecare _paintHeader
1032
+ h.addEventListener("click", function (e) {
1033
+ if (hasTf && e.target.closest && e.target.closest(".trc-tf-btn")) {
1034
+ e.stopPropagation();
1035
+ self2._toggleTfMenu();
1036
+ return;
1037
+ }
1038
+ if (clickable) self2._toggleSearch();
1039
+ });
1026
1040
  this.el.appendChild(h);
1027
1041
  this._header = h;
1028
1042
  }
@@ -1042,13 +1056,52 @@
1042
1056
  var up = chg !== null && chg >= 0;
1043
1057
  this._header.innerHTML =
1044
1058
  '<span style="color:' + (o.tagText || "#e6edf3") + ';font-size:17px;">' + (inf.ticker || "") + "</span>" +
1045
- (inf.tf ? '<span style="color:' + o.textColor + ';font-weight:700;font-size:12px;">' + inf.tf + "</span>" : "") +
1059
+ (this.opt.timeframes && this.opt.timeframes.list && this.opt.timeframes.list.length
1060
+ ? '<span class="trc-tf-btn" title="Change timeframe" style="color:' + o.textColor + ';font-weight:700;font-size:12px;' +
1061
+ 'border:1px solid ' + o.separatorColor + ';border-radius:6px;padding:1px 8px;cursor:pointer;">' +
1062
+ (this._activeTfLabel() || inf.tf || "TF") + " \u25BE</span>"
1063
+ : (inf.tf ? '<span style="color:' + o.textColor + ';font-weight:700;font-size:12px;">' + inf.tf + "</span>" : "")) +
1046
1064
  (px ? '<span style="color:' + (up ? o.upColor : o.downColor) + ';">' + px + "</span>" : "") +
1047
1065
  (chg !== null ? '<span style="color:' + (up ? o.upColor : o.downColor) + ';font-weight:700;font-size:12px;">' +
1048
1066
  (up ? "+" : "") + chg.toFixed(2) + "%</span>" : "") +
1049
1067
  (inf.title ? '<span style="color:' + o.textColor + ';font-weight:600;font-size:12px;">' + inf.title + "</span>" : "");
1050
1068
  };
1051
1069
 
1070
+ Chart.prototype._activeTfLabel = function () {
1071
+ var t = this.opt.timeframes;
1072
+ if (!t || !t.list) return null;
1073
+ for (var i = 0; i < t.list.length; i++) if (t.list[i].value === t.active) return t.list[i].label;
1074
+ return null;
1075
+ };
1076
+ Chart.prototype._toggleTfMenu = function () {
1077
+ var self = this, o = this.opt, t = o.timeframes;
1078
+ if (this._tfMenu) { this._tfMenu.remove(); this._tfMenu = null; return; }
1079
+ if (!t || !t.list) return;
1080
+ var m = document.createElement("div");
1081
+ m.className = "trc-tf-menu";
1082
+ m.style.cssText = "position:absolute;left:" + (o.ui ? 56 : 12) + "px;top:34px;z-index:8;" +
1083
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";border-radius:12px;padding:6px;" +
1084
+ "color:" + o.textColor + ";font:12px -apple-system,'Segoe UI',sans-serif;box-shadow:0 8px 30px rgba(0,0,0,0.35);" +
1085
+ "display:grid;grid-template-columns:repeat(4,1fr);gap:2px;max-height:70%;overflow:auto;";
1086
+ t.list.forEach(function (it) {
1087
+ var b = document.createElement("button");
1088
+ b.type = "button";
1089
+ b.textContent = it.label;
1090
+ b.style.cssText = "border:none;background:" + (it.value === t.active ? "rgba(99,102,241,0.25)" : "none") +
1091
+ ";color:inherit;font:inherit;font-weight:700;border-radius:8px;padding:6px 8px;cursor:pointer;text-align:center;";
1092
+ b.addEventListener("click", function () {
1093
+ m.remove(); self._tfMenu = null;
1094
+ if (it.value !== t.active && t.onSelect) t.onSelect(it.value);
1095
+ });
1096
+ m.appendChild(b);
1097
+ });
1098
+ this.el.appendChild(m);
1099
+ var hM = m.offsetHeight || 0, maxTopM = this._plotH() - hM - 6;
1100
+ if (34 > maxTopM) m.style.top = Math.max(6, maxTopM) + "px";
1101
+ this._tfMenu = m;
1102
+ this._armOutsideClose(m, this._header, function () { m.remove(); self._tfMenu = null; });
1103
+ };
1104
+
1052
1105
  // The indicator legend with LIVE numbers: label in the series color plus
1053
1106
  // the value under the crosshair (or the last bar when the cursor is away).
1054
1107
  Chart.prototype._paintLegend = function (barTime) {
@@ -1354,6 +1407,61 @@
1354
1407
  } else if (d.type === "trend") {
1355
1408
  c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.stroke();
1356
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); }
1357
1465
  } else if (d.type === "rect") {
1358
1466
  c.save(); c.globalAlpha = 0.12;
1359
1467
  c.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
@@ -1404,7 +1512,7 @@
1404
1512
  var x1 = isNum(d.t1) ? this.timeToX(d.t1) : null, y1 = isNum(d.p1) ? pp.toY(d.p1) : null;
1405
1513
  var x2 = isNum(d.t2) ? this.timeToX(d.t2) : null, y2 = isNum(d.p2) ? pp.toY(d.p2) : null;
1406
1514
  if (d.id === this._selected && x2 !== null && Math.abs(x - x2) < 7 && Math.abs(y - y2) < 7 &&
1407
- (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" };
1408
1516
  if (d.id === this._selected && d.type === "text") {
1409
1517
  var fsz = d.size || 12, tw2 = d._tw || (d.text || "").length * fsz * 0.62;
1410
1518
  if (Math.abs(x - (x1 + tw2 + 6)) < 8 && Math.abs(y - y1) < 8) return { d: d, handle: "size" };
@@ -1412,7 +1520,7 @@
1412
1520
  if (d.id === this._selected && d.type === "trend" && Math.abs(x - x1) < 7 && Math.abs(y - y1) < 7) return { d: d, handle: "p1" };
1413
1521
  if (d.type === "hline") { if (Math.abs(y - y1) < 6) return { d: d, handle: null }; }
1414
1522
  else if (d.type === "trend") { if (distToSeg(x, y, { x: x1, y: y1 }, { x: x2, y: y2 }) < 6) return { d: d, handle: null }; }
1415
- else if (d.type === "rect" || d.type === "fib") {
1523
+ else if (d.type === "rect" || d.type === "fib" || d.type === "measure") {
1416
1524
  if (x >= Math.min(x1, x2) - 4 && x <= Math.max(x1, x2) + 4 &&
1417
1525
  y >= Math.min(y1, y2) - 4 && y <= Math.max(y1, y2) + 4) return { d: d, handle: null };
1418
1526
  } else if (d.type === "text") {