traderead-charts 1.3.3 → 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,34 @@
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
+
3
32
  ## 1.3.3 — 2026-09-29
4
33
 
5
34
  - **The view can be dragged past the last bar by half the plot**, not by ten
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.3",
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");
@@ -763,6 +781,14 @@
763
781
  // on a phone at 2 px a bar: the latest candles stayed pinned to the price
764
782
  // axis and could not be brought to the middle to zoom on them (owner's
765
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
+ };
766
792
  Chart.prototype._maxRight = function () {
767
793
  return this.bars.length - 1 + Math.max(this.opt.rightPadBars * 2, (this._plotW() * 0.5) / (this.barSpacing || 1));
768
794
  };
@@ -1235,11 +1261,18 @@
1235
1261
  };
1236
1262
 
1237
1263
 
1238
- Chart.prototype.addPriceLine = function (price, color, label) {
1239
- 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; }) : [];
1240
1274
  this._paint();
1241
1275
  };
1242
- Chart.prototype.clearPriceLines = function () { this.priceLines = []; this._paint(); };
1243
1276
  Chart.prototype.onCrosshair = function (cb) { this.crosshairCb = cb; };
1244
1277
  // cb({firstVisible, lastVisible, barsLeft}) after every pan/zoom; fires at
1245
1278
  // most once per paint. barsLeft small = time to prepend history.
@@ -1268,11 +1301,16 @@
1268
1301
  this.el.style.cursor = t ? "crosshair" : "";
1269
1302
  this._touchLock();
1270
1303
  };
1271
- // While a tool is armed or a shape is being dragged, the chart owns the
1272
- // touch: without touch-action none the browser steals the gesture, fires
1273
- // 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.
1274
1312
  Chart.prototype._touchLock = function () {
1275
- this.overlay.style.touchAction = (this.tool || this._draft || this._dragDraw || this._sepDrag || this._axisDrag) ? "none" : "";
1313
+ this.overlay.style.touchAction = "none";
1276
1314
  };
1277
1315
  Chart.prototype.onToolDone = function (cb) { this._toolDoneCb = cb; };
1278
1316
  Chart.prototype.clearDrawings = function () { this.drawings = []; this._draft = null; this._selected = null; this._persist(); this._paintDrawings(); };
@@ -1839,16 +1877,37 @@
1839
1877
  for (i = 0; i < tags.length; i++) {
1840
1878
  var t = tags[i];
1841
1879
  var ty = pp.toY(t.price);
1842
- if (ty < pp.y0 || ty > pp.y0 + pp.h) continue;
1843
- c.strokeStyle = t.color; c.lineWidth = 1; c.setLineDash([4, 3]);
1844
- c.beginPath(); c.moveTo(0, Math.round(ty) + 0.5); c.lineTo(W, Math.round(ty) + 0.5); c.stroke();
1845
- 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
+ }
1846
1892
  var txt = fmtPrice(t.price, dec);
1847
1893
  c.fillStyle = t._last ? t.color : o.tagBg;
1848
1894
  c.fillRect(W, ty - 9, o.priceAxisWidth, 18);
1849
1895
  c.fillStyle = t._last ? "#0d1117" : o.tagText;
1850
1896
  c.textBaseline = "middle";
1851
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
+ }
1852
1911
  }
1853
1912
 
1854
1913
  // markers
@@ -1968,7 +2027,7 @@
1968
2027
  if (self._axisDrag) {
1969
2028
  var ad = self._axisDrag;
1970
2029
  if (ad.axis === "price") self._vZoom = clamp(ad.z0 * Math.exp((self._cross.y - ad.y0) * 0.004), 0.15, 8);
1971
- 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);
1972
2031
  self._paint();
1973
2032
  return;
1974
2033
  }
@@ -2166,7 +2225,7 @@
2166
2225
  }
2167
2226
  var anchor = self.xToIndex(x);
2168
2227
  var k = Math.exp(-e.deltaY * 0.0015);
2169
- self.barSpacing = clamp(self.barSpacing * k, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2228
+ self.barSpacing = clamp(self.barSpacing * k, self._minSpacing(), self.opt.maxBarSpacing);
2170
2229
  self.rightIndex = anchor - (self.xToIndex(x) - self.rightIndex);
2171
2230
  self._paint();
2172
2231
  }, { passive: false });
@@ -2231,7 +2290,7 @@
2231
2290
  var aax = e.touches[0].clientX - r.left, aay = e.touches[0].clientY - r.top;
2232
2291
  var ad2 = self._axisDrag;
2233
2292
  if (ad2.axis === "price") self._vZoom = clamp(ad2.z0 * Math.exp((aay - ad2.y0) * 0.004), 0.15, 8);
2234
- 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);
2235
2294
  self._paint();
2236
2295
  e.preventDefault();
2237
2296
  return;
@@ -2269,7 +2328,7 @@
2269
2328
  var g0 = e.touches[0], g1 = e.touches[1];
2270
2329
  var pd = Math.hypot(g0.clientX - g1.clientX, g0.clientY - g1.clientY) || 1;
2271
2330
  var pmx = (g0.clientX + g1.clientX) / 2 - r.left;
2272
- self.barSpacing = clamp(pinch.spacing0 * pd / pinch.d0, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2331
+ self.barSpacing = clamp(pinch.spacing0 * pd / pinch.d0, self._minSpacing(), self.opt.maxBarSpacing);
2273
2332
  // the bar that was under the fingers stays under them (and follows
2274
2333
  // them: two fingers also pan)
2275
2334
  self.rightIndex = clamp(pinch.anchor - (self.xToIndex(pmx) - self.rightIndex), 0, self._maxRight());
@@ -2365,7 +2424,7 @@
2365
2424
  }
2366
2425
 
2367
2426
  global.TRCharts = {
2368
- version: "1.0.0",
2427
+ version: "1.3.5",
2369
2428
  themes: THEMES,
2370
2429
  resample: resample,
2371
2430
  createChart: function (el, options) { return new Chart(el, options); },