traderead-charts 1.3.2 → 1.3.5

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,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.5 — 2026-10-04
4
+
5
+ - **Price lines can carry a trade plan.** `addPriceLine(price, color, label,
6
+ opts)` now writes the label on the line (the price stays on the axis), and
7
+ takes `opts.fit` and `opts.group`:
8
+ - `fit: true` widens the price scale to keep the line on screen, up to 0.6×
9
+ the visible bars' own range — a far second target used to squeeze the
10
+ candles into half the pane, so beyond that it is left out;
11
+ - a labelled line still outside the frame is pinned to the pane's edge with
12
+ an arrow, so a plan never silently loses a level;
13
+ - `group: "name"` lets `clearPriceLines("name")` remove just that set;
14
+ `clearPriceLines()` with no argument still clears every line.
15
+ - Unlabelled lines behave exactly as before.
16
+
17
+ ## 1.3.4 — 2026-09-30
18
+
19
+ - **A pinch on a phone zooms the chart, not the page.** The plot now always
20
+ carries `touch-action: none`. 1.3.3 cancelled the pinch in `touchmove`,
21
+ but the browser decides to zoom the page at `touchstart`, before that
22
+ handler can stop it — on phones the whole page still grew. Nothing else
23
+ changes: every one-finger move on the plot was already the chart's, so the
24
+ page never scrolled from there.
25
+ - **The version inside the file matches the package again**: the banner read
26
+ 1.3.0 and `TRCharts.version` 1.0.0 through 1.3.3.
27
+ - **Zoom out reaches as far on a phone as on a laptop.** The narrowest bar was
28
+ 1.5 px, so a 310-px phone plot held 200 bars while a laptop held 700; the
29
+ floor is now whatever fits 750 bars in the plot, when that is smaller than
30
+ 1.5 px (never under 0.25 px). Laptops are practically unchanged.
31
+
32
+ ## 1.3.3 — 2026-09-29
33
+
34
+ - **The view can be dragged past the last bar by half the plot**, not by ten
35
+ bars. On a phone at 2 px a bar ten bars were 20 px: the latest candles
36
+ stayed pinned to the price axis and could not be brought to the middle to
37
+ zoom on them. The limit is now in pixels; wide bars keep the old ten.
38
+ - **Pinch zooms around the fingers**, and the bar under them stays under
39
+ them. It used to zoom from the right edge only.
40
+ - **Pinch measures the real distance between the fingers.** A pinch made
41
+ with one finger above the other has almost no horizontal spread; measuring
42
+ only that made the zoom jump.
43
+ - **A pinch on the chart never zooms the page instead.** iOS ignores
44
+ `user-scalable=no`: the touch handler now claims the gesture, and Safari's
45
+ own `gesturestart` / `gesturechange` are cancelled on the chart.
46
+
3
47
  ## 1.3.2 — 2026-09-20
4
48
 
5
49
  - **README install example unpinned from 1.2.0.** The copy-paste snippet on
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.3.1/src/traderead-charts.js"></script>
61
+ <script src="https://cdn.jsdelivr.net/npm/traderead-charts@1.3.3/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.3.2",
3
+ "version": "1.3.5",
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.3.0
2
+ * TradeRead Charts v1.3.5
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
@@ -125,6 +125,21 @@
125
125
  if (v < min) min = v;
126
126
  if (v > max) max = v;
127
127
  }
128
+ // Price lines added with { fit: true } — a trade plan's entry, stop and
129
+ // targets — widen the price scale so the plan is on screen. One sitting
130
+ // more than 0.6x the bars' own range away is left out: a far Target 2
131
+ // squeezed the candles into half the pane (4 Oct 2026). It is pinned to
132
+ // the pane's edge instead (see the price-line paint).
133
+ if (this.kind === "price" && isFinite(min) && isFinite(max)) {
134
+ var span = max - min, pls = this.chart.priceLines, fmin = min, fmax = max;
135
+ for (i = 0; i < pls.length; i++) {
136
+ v = pls[i].price;
137
+ if (!pls[i].fit || !isNum(v) || v < min - 0.6 * span || v > max + 0.6 * span) continue;
138
+ if (v < fmin) fmin = v;
139
+ if (v > fmax) fmax = v;
140
+ }
141
+ min = fmin; max = fmax;
142
+ }
128
143
  if (!isFinite(min) || !isFinite(max)) { min = 0; max = 1; }
129
144
  if (min === max) { min -= 0.5; max += 0.5; }
130
145
  var pad = (max - min) * this.chart.opt.autoScalePadPct;
@@ -178,6 +193,9 @@
178
193
  c.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;display:block;";
179
194
  container.appendChild(c);
180
195
  });
196
+ // from the first touch: the browser reads touch-action when the finger
197
+ // lands, so it must be in place before any gesture (see _touchLock)
198
+ this.overlay.style.touchAction = "none";
181
199
  this.ctx = this.canvas.getContext("2d");
182
200
  this.dctx = this.drawCanvas.getContext("2d");
183
201
  this.octx = this.overlay.getContext("2d");
@@ -758,6 +776,22 @@
758
776
  Chart.prototype.xToIndex = function (x) {
759
777
  return this.rightIndex + this.opt.rightPadBars - (this._plotW() - x) / this.barSpacing;
760
778
  };
779
+ // How far past the last bar the view may be dragged: the pad, or half the
780
+ // plot, whichever is more — in pixels, not bars. Ten bars of slack was 20 px
781
+ // on a phone at 2 px a bar: the latest candles stayed pinned to the price
782
+ // axis and could not be brought to the middle to zoom on them (owner's
783
+ // find, 29 Sep 2026).
784
+ // Zoomed all the way out, a phone shows as many bars as a laptop: the floor
785
+ // is the option (1.5 px a bar) or whatever fits 750 bars in the plot,
786
+ // whichever is smaller — a 310-px phone plot held 200 bars at 1.5 px
787
+ // (owner's find, 30 Sep 2026: "pe mobil pot să fac un zoom out limitat").
788
+ // Never under 0.25 px; each bar is still drawn at least a pixel wide.
789
+ Chart.prototype._minSpacing = function () {
790
+ return Math.max(0.25, Math.min(this.opt.minBarSpacing, this._plotW() / 750));
791
+ };
792
+ Chart.prototype._maxRight = function () {
793
+ return this.bars.length - 1 + Math.max(this.opt.rightPadBars * 2, (this._plotW() * 0.5) / (this.barSpacing || 1));
794
+ };
761
795
  Chart.prototype._visibleRange = function () {
762
796
  var lo = Math.floor(this.xToIndex(0)), hi = Math.ceil(this.xToIndex(this._plotW()));
763
797
  return [clamp(lo, 0, this.bars.length - 1), clamp(hi, 0, this.bars.length - 1)];
@@ -1227,11 +1261,18 @@
1227
1261
  };
1228
1262
 
1229
1263
 
1230
- Chart.prototype.addPriceLine = function (price, color, label) {
1231
- this.priceLines.push({ price: price, color: color || "#8b949e", label: label });
1264
+ // opts.fit: widen the price scale to keep the line in view (see the pane
1265
+ // scale). opts.group: a name, so clearPriceLines(name) removes only that
1266
+ // set. The label is written on the line; the price stays on the axis.
1267
+ Chart.prototype.addPriceLine = function (price, color, label, opts) {
1268
+ opts = opts || {};
1269
+ this.priceLines.push({ price: price, color: color || "#8b949e", label: label, fit: !!opts.fit, group: opts.group || null });
1270
+ this._paint();
1271
+ };
1272
+ Chart.prototype.clearPriceLines = function (group) {
1273
+ this.priceLines = group ? this.priceLines.filter(function (l) { return l.group !== group; }) : [];
1232
1274
  this._paint();
1233
1275
  };
1234
- Chart.prototype.clearPriceLines = function () { this.priceLines = []; this._paint(); };
1235
1276
  Chart.prototype.onCrosshair = function (cb) { this.crosshairCb = cb; };
1236
1277
  // cb({firstVisible, lastVisible, barsLeft}) after every pan/zoom; fires at
1237
1278
  // most once per paint. barsLeft small = time to prepend history.
@@ -1260,11 +1301,16 @@
1260
1301
  this.el.style.cursor = t ? "crosshair" : "";
1261
1302
  this._touchLock();
1262
1303
  };
1263
- // While a tool is armed or a shape is being dragged, the chart owns the
1264
- // touch: without touch-action none the browser steals the gesture, fires
1265
- // a cancel and kills the line mid-draw (paid for once in production).
1304
+ // The plot owns every touch, always. Without touch-action none the browser
1305
+ // steals the gesture: mid-draw it fires a cancel and kills the line (paid
1306
+ // for once in production), and a pinch zoomed the whole PAGE instead of
1307
+ // the chart on phones (owner's find, 30 Sep 2026) — the browser decides
1308
+ // that at touchstart, before any preventDefault in touchmove can stop it.
1309
+ // Nothing is lost: every one-finger move on the plot was already the
1310
+ // chart's (touchmove prevents the default), so the page never scrolled
1311
+ // from here anyway.
1266
1312
  Chart.prototype._touchLock = function () {
1267
- this.overlay.style.touchAction = (this.tool || this._draft || this._dragDraw || this._sepDrag || this._axisDrag) ? "none" : "";
1313
+ this.overlay.style.touchAction = "none";
1268
1314
  };
1269
1315
  Chart.prototype.onToolDone = function (cb) { this._toolDoneCb = cb; };
1270
1316
  Chart.prototype.clearDrawings = function () { this.drawings = []; this._draft = null; this._selected = null; this._persist(); this._paintDrawings(); };
@@ -1831,16 +1877,37 @@
1831
1877
  for (i = 0; i < tags.length; i++) {
1832
1878
  var t = tags[i];
1833
1879
  var ty = pp.toY(t.price);
1834
- if (ty < pp.y0 || ty > pp.y0 + pp.h) continue;
1835
- c.strokeStyle = t.color; c.lineWidth = 1; c.setLineDash([4, 3]);
1836
- c.beginPath(); c.moveTo(0, Math.round(ty) + 0.5); c.lineTo(W, Math.round(ty) + 0.5); c.stroke();
1837
- c.setLineDash([]);
1880
+ // A labelled level outside the frame (a far target) is pinned to the
1881
+ // pane's edge with an arrow, so the plan never silently loses a level;
1882
+ // an unlabelled line outside the frame stays hidden, as before.
1883
+ var offAbove = ty < pp.y0, offBelow = ty > pp.y0 + pp.h;
1884
+ if ((offAbove || offBelow) && !(t.label && !t._last)) continue;
1885
+ if (offAbove) ty = pp.y0 + 10;
1886
+ else if (offBelow) ty = pp.y0 + pp.h - 10;
1887
+ if (!offAbove && !offBelow) {
1888
+ c.strokeStyle = t.color; c.lineWidth = 1; c.setLineDash([4, 3]);
1889
+ c.beginPath(); c.moveTo(0, Math.round(ty) + 0.5); c.lineTo(W, Math.round(ty) + 0.5); c.stroke();
1890
+ c.setLineDash([]);
1891
+ }
1838
1892
  var txt = fmtPrice(t.price, dec);
1839
1893
  c.fillStyle = t._last ? t.color : o.tagBg;
1840
1894
  c.fillRect(W, ty - 9, o.priceAxisWidth, 18);
1841
1895
  c.fillStyle = t._last ? "#0d1117" : o.tagText;
1842
1896
  c.textBaseline = "middle";
1843
1897
  c.fillText(txt, W + 5, ty);
1898
+ // the line says what it is ("TP1", "SL"), just above it at the right edge
1899
+ if (t.label && !t._last) {
1900
+ c.save();
1901
+ c.font = "700 10.5px -apple-system, 'Segoe UI', sans-serif";
1902
+ var lbl = offAbove ? "\u25B2 " + t.label : offBelow ? "\u25BC " + t.label : t.label;
1903
+ var lw = c.measureText(lbl).width;
1904
+ c.globalAlpha = 0.85; c.fillStyle = o.tagBg;
1905
+ c.fillRect(W - lw - 14, ty - 16, lw + 10, 14);
1906
+ c.globalAlpha = 1; c.fillStyle = t.color;
1907
+ c.textAlign = "right"; c.textBaseline = "middle";
1908
+ c.fillText(lbl, W - 9, offAbove ? ty + 1 : ty - 9);
1909
+ c.restore();
1910
+ }
1844
1911
  }
1845
1912
 
1846
1913
  // markers
@@ -1938,7 +2005,7 @@
1938
2005
  function step(now) {
1939
2006
  var dt = now - last; last = now;
1940
2007
  self.rightIndex = clamp(self.rightIndex - glide.v * dt / self.barSpacing,
1941
- 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2008
+ 0, self._maxRight());
1942
2009
  self._paint();
1943
2010
  glide.v *= Math.pow(0.94, dt / 16.7);
1944
2011
  if (Math.abs(glide.v) >= 0.05) glide.raf = requestAnimationFrame(step);
@@ -1960,7 +2027,7 @@
1960
2027
  if (self._axisDrag) {
1961
2028
  var ad = self._axisDrag;
1962
2029
  if (ad.axis === "price") self._vZoom = clamp(ad.z0 * Math.exp((self._cross.y - ad.y0) * 0.004), 0.15, 8);
1963
- else self.barSpacing = clamp(ad.s0 * Math.exp((self._cross.x - ad.x0) * 0.004), self.opt.minBarSpacing, self.opt.maxBarSpacing);
2030
+ else self.barSpacing = clamp(ad.s0 * Math.exp((self._cross.x - ad.x0) * 0.004), self._minSpacing(), self.opt.maxBarSpacing);
1964
2031
  self._paint();
1965
2032
  return;
1966
2033
  }
@@ -2003,7 +2070,7 @@
2003
2070
  }
2004
2071
  glide.lastX = nowX; glide.lastT = nowT;
2005
2072
  self.rightIndex = drag.right0 + (drag.x0 - nowX) / self.barSpacing;
2006
- self.rightIndex = clamp(self.rightIndex, 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2073
+ self.rightIndex = clamp(self.rightIndex, 0, self._maxRight());
2007
2074
  if (drag.vpan) {
2008
2075
  var us = self.panes[0].h * (1 - self.opt.volumeHeightPct * 0.35) || 1;
2009
2076
  self._vOff = drag.off0 + (e.clientY - r.top - drag.y0) / us;
@@ -2152,13 +2219,13 @@
2152
2219
  // way every charting tool treats it. Vertical wheel stays zoom.
2153
2220
  if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
2154
2221
  self.rightIndex = clamp(self.rightIndex + e.deltaX / self.barSpacing,
2155
- 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2222
+ 0, self._maxRight());
2156
2223
  self._paint();
2157
2224
  return;
2158
2225
  }
2159
2226
  var anchor = self.xToIndex(x);
2160
2227
  var k = Math.exp(-e.deltaY * 0.0015);
2161
- self.barSpacing = clamp(self.barSpacing * k, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2228
+ self.barSpacing = clamp(self.barSpacing * k, self._minSpacing(), self.opt.maxBarSpacing);
2162
2229
  self.rightIndex = anchor - (self.xToIndex(x) - self.rightIndex);
2163
2230
  self._paint();
2164
2231
  }, { passive: false });
@@ -2209,8 +2276,12 @@
2209
2276
  if (self._draft) { self.drawings.push(self._draft); self._selected = self._draft.id; self._draft = null; self._persist(); self._paintDrawings(); }
2210
2277
  self._dragDraw = null; self._sepDrag = null; drag = null;
2211
2278
  self._touchLock();
2212
- var dx = e.touches[0].clientX - e.touches[1].clientX;
2213
- pinch = { d0: Math.abs(dx) || 1, spacing0: self.barSpacing };
2279
+ // Zoom around the fingers, on their real distance: a pinch made with
2280
+ // one finger above the other has almost no horizontal spread, and
2281
+ // measuring only that made the zoom jump.
2282
+ var f0 = e.touches[0], f1 = e.touches[1];
2283
+ pinch = { d0: Math.hypot(f0.clientX - f1.clientX, f0.clientY - f1.clientY) || 1, spacing0: self.barSpacing,
2284
+ anchor: self.xToIndex((f0.clientX + f1.clientX) / 2 - r.left) };
2214
2285
  }
2215
2286
  }, { passive: true });
2216
2287
  el.addEventListener("touchmove", function (e) {
@@ -2219,7 +2290,7 @@
2219
2290
  var aax = e.touches[0].clientX - r.left, aay = e.touches[0].clientY - r.top;
2220
2291
  var ad2 = self._axisDrag;
2221
2292
  if (ad2.axis === "price") self._vZoom = clamp(ad2.z0 * Math.exp((aay - ad2.y0) * 0.004), 0.15, 8);
2222
- else self.barSpacing = clamp(ad2.s0 * Math.exp((aax - ad2.x0) * 0.004), self.opt.minBarSpacing, self.opt.maxBarSpacing);
2293
+ else self.barSpacing = clamp(ad2.s0 * Math.exp((aax - ad2.x0) * 0.004), self._minSpacing(), self.opt.maxBarSpacing);
2223
2294
  self._paint();
2224
2295
  e.preventDefault();
2225
2296
  return;
@@ -2254,9 +2325,16 @@
2254
2325
  return;
2255
2326
  }
2256
2327
  if (pinch && e.touches.length === 2) {
2257
- var d = Math.abs(e.touches[0].clientX - e.touches[1].clientX) || 1;
2258
- self.barSpacing = clamp(pinch.spacing0 * d / pinch.d0, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2328
+ var g0 = e.touches[0], g1 = e.touches[1];
2329
+ var pd = Math.hypot(g0.clientX - g1.clientX, g0.clientY - g1.clientY) || 1;
2330
+ var pmx = (g0.clientX + g1.clientX) / 2 - r.left;
2331
+ self.barSpacing = clamp(pinch.spacing0 * pd / pinch.d0, self._minSpacing(), self.opt.maxBarSpacing);
2332
+ // the bar that was under the fingers stays under them (and follows
2333
+ // them: two fingers also pan)
2334
+ self.rightIndex = clamp(pinch.anchor - (self.xToIndex(pmx) - self.rightIndex), 0, self._maxRight());
2259
2335
  self._paint();
2336
+ // the page must not zoom instead: iOS ignores user-scalable=no
2337
+ e.preventDefault();
2260
2338
  } else if (drag && e.touches.length === 1) {
2261
2339
  var tNowT = performance.now(), tNowX = e.touches[0].clientX - r.left;
2262
2340
  if (glide.lastT) {
@@ -2265,7 +2343,7 @@
2265
2343
  }
2266
2344
  glide.lastX = tNowX; glide.lastT = tNowT;
2267
2345
  self.rightIndex = drag.right0 + (drag.x0 - tNowX) / self.barSpacing;
2268
- self.rightIndex = clamp(self.rightIndex, 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2346
+ self.rightIndex = clamp(self.rightIndex, 0, self._maxRight());
2269
2347
  if (drag.vpan) {
2270
2348
  var us2 = self.panes[0].h * (1 - self.opt.volumeHeightPct * 0.35) || 1;
2271
2349
  self._vOff = drag.off0 + (e.touches[0].clientY - r.top - drag.y0) / us2;
@@ -2291,6 +2369,9 @@
2291
2369
  self._touchLock();
2292
2370
  }
2293
2371
  el.addEventListener("touchend", endTouch, { passive: true });
2372
+ // Safari's own pinch events: without this it zooms the whole page.
2373
+ el.addEventListener("gesturestart", function (e) { e.preventDefault(); });
2374
+ el.addEventListener("gesturechange", function (e) { e.preventDefault(); });
2294
2375
  // a browser-fired cancel must COMMIT the in-flight shape, not destroy it
2295
2376
  el.addEventListener("touchcancel", endTouch, { passive: true });
2296
2377
  };
@@ -2343,7 +2424,7 @@
2343
2424
  }
2344
2425
 
2345
2426
  global.TRCharts = {
2346
- version: "1.0.0",
2427
+ version: "1.3.5",
2347
2428
  themes: THEMES,
2348
2429
  resample: resample,
2349
2430
  createChart: function (el, options) { return new Chart(el, options); },