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 +17 -0
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/traderead-charts.js +119 -11
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.
|
|
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": [
|
package/src/traderead-charts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* TradeRead Charts v1.
|
|
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"]
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
(
|
|
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") {
|