Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
# Change Log

## [0.9.2] - 2026-03-05 - Gradient Fill, Matrix Inverse, Array Optimizations & Plot Fixes
## [0.9.2] - 2026-03-06 - Drawing Object Method Syntax, Gradient Fill, Matrix & Array Improvements

### Added

- **Gradient Fill (`fill()`)**: Added support for Pine Script's gradient fill signature — `fill(plot1, plot2, top_value, bottom_value, top_color, bottom_color)`. The `FillHelper` now detects the gradient form by checking whether the third argument is a number (gradient) or a string/color (simple fill), and stores per-bar `top_value`/`bottom_value`/`top_color`/`bottom_color` data for rendering.
- **Method-Call Syntax on Drawing Instances**: `LineObject`, `LabelObject`, and `BoxObject` now carry delegate setter/getter methods directly on the instance (e.g., `myLine.set_x2(x)`, `myBox.set_right(r)`, `myLabel.set_text(t)`). Each delegate forwards to the owning helper so the plot sync (`_syncToPlot`) fires correctly. Enables Pine Script patterns where drawing objects stored in UDTs or arrays are mutated via method syntax.
- **Gradient Fill (`fill()`)**: Added support for Pine Script's gradient fill signature — `fill(plot1, plot2, top_value, bottom_value, top_color, bottom_color)`. The `FillHelper` detects the gradient form (third argument is a number) and stores per-bar `top_value`/`bottom_value`/`top_color`/`bottom_color` data for the renderer.
- **Typed Generic Function Parameters**: The Pine Script parser now correctly handles generic type annotations in function parameter lists (e.g., `array<float> src`, `map<string, float> data`). Previously these caused parse errors.

### Fixed

- **`matrix.inv()` — Full NxN Support**: Rewrote `matrix.inv()` from a 2×2-only implementation to a general Gauss-Jordan elimination with partial pivoting, supporting any square matrix of arbitrary size. Singular matrices (pivot < 1e-14) correctly return a NaN matrix.
- **`matrix.pinv()` — Real Pseudoinverse**: Rewrote `matrix.pinv()` from a placeholder stub to a correct Moore-Penrose pseudoinverse: square matrices use `inv()`, tall matrices (m > n) use `(AᵀA)⁻¹Aᵀ`, and wide matrices (m < n) use `Aᵀ(AAᵀ)⁻¹`.
- **`array.min()` / `array.max()` Performance**: Added an O(N) fast path for the common `nth=0` case (find absolute min/max) instead of always sorting the full array O(N log N). Sorting is still used only when `nth > 0`.
- **`array.median()` Performance**: Replaced the `for...of` copy + complex sort comparator with a direct index loop and simple numeric sort for faster execution.
- **`array.percentile_linear_interpolation()` Performance**: Validate and copy the array in a single pass (eliminating the separate `validValues` allocation), then sort once.
- **`array.percentile_nearest_rank()` Performance**: Same single-pass validate-and-copy optimization as `percentile_linear_interpolation`.
- **`isPlot()` with Undefined Title**: Fixed the `isPlot()` helper check to accept plots that have no `title` property but do have a `_plotKey` property (e.g., plots created via `fill()` or accessed by callsite ID). Previously these were not recognised as plot objects, causing `fill()` to misidentify its arguments (contribution by @dcaoyuan, [#142](https://github.com/QuantForgeOrg/PineTS/issues/142)).
- **UDT Thunk Resolution for Drawing Object Fields**: When a `var` UDT instance contains fields initialised with factory calls (e.g., `line.new(...)`, `box.new(...)`), those fields are now correctly resolved as thunks on bar 0 inside `initVar`. Previously the thunk-wrapped factory results were stored as raw functions in the UDT field, causing the drawing object to never be created.
- **Typed Array Type Inference for Object Types**: `inferValueType()` no longer throws `"Cannot infer type from value"` when called with an object (e.g., a `LineObject` or `BoxObject`). It now returns `PineArrayType.any`, allowing `array<line>` and similar typed arrays to work correctly.
- **Non-Computed Namespace Property Access in `$.param()`**: Fixed `ExpressionTransformer` incorrectly wrapping namespace constant accesses (e.g., `label.style_label_down`, `line.style_dashed`) in `$.get()` calls when they appeared inside function arguments. The transformer now detects non-computed member access on `NAMESPACES_LIKE` identifiers and leaves them untransformed.
- **`histbase` Type in `PlotOptions`**: Fixed the `histbase` field in the `PlotOptions` TypeScript type from `boolean` to `number`, matching the actual Pine Script `plot(histbase=50)` signature.
- **For-Loop `MemberExpression` Recursion**: Fixed user variable identifiers inside method calls in `for` loops (e.g., `lineMatrix.rows()`) not being transformed. The `MemberExpression` visitor in `transformForStatement` now recurses into the object node after transformation so nested identifiers are correctly resolved.
- **Multiline `and` / Comparison Expressions**: Fixed the Pine Script parser dropping continuation lines in `and`/`&&` chains and comparison expressions spanning multiple lines. `skipNewlines(true)` is now called after the operator.
- **`matrix.inv()` — Full NxN Support**: Rewrote `matrix.inv()` from a 2×2-only implementation to Gauss-Jordan elimination with partial pivoting, supporting any square matrix. Singular matrices (pivot < 1e-14) return a NaN matrix.
- **`matrix.pinv()` — Real Pseudoinverse**: Rewrote `matrix.pinv()` from a placeholder stub to a correct Moore-Penrose pseudoinverse: square → `inv()`, tall (m > n) → `(AᵀA)⁻¹Aᵀ`, wide (m < n) → `Aᵀ(AAᵀ)⁻¹`.
- **`array.min()` / `array.max()` Performance**: Added an O(N) fast path for the common `nth=0` case instead of always sorting O(N log N).
- **`array.median()`, `percentile_linear_interpolation()`, `percentile_nearest_rank()` Performance**: Single-pass copy-and-validate optimizations.
- **`isPlot()` with Undefined Title**: Fixed `isPlot()` to accept plot objects that have `_plotKey` but no `title` property (e.g., fill plots created via callsite ID), preventing `fill()` from misidentifying its arguments (contribution by @dcaoyuan, [#142](https://github.com/QuantForgeOrg/PineTS/issues/142)).
- **Duplicate `map` in `CONTEXT_PINE_VARS`**: Removed an accidental duplicate `'map'` entry from `settings.ts`.

## [0.9.1] - 2026-03-04 - Enum Values, ATR/DMI/Supertrend Fixes, UDT & Transpiler Improvements

Expand Down
15 changes: 15 additions & 0 deletions src/Context.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Input } from './namespaces/input/input.index';
import PineMath from './namespaces/math/math.index';
import { PineRequest } from './namespaces/request/request.index';
import TechnicalAnalysis from './namespaces/ta/ta.index';
import { PineTypeObject } from './namespaces/PineTypeObject';
import { Series } from './Series';
import { Log } from './namespaces/Log';
import { Str } from './namespaces/Str';
Expand Down Expand Up @@ -535,6 +536,20 @@ export class Context {
src = src();
}

// Resolve thunks inside PineTypeObject fields (UDT instances).
// When a `var` declaration initializes a UDT with factory calls like
// `MyType.new(line.new(...), label.new(...))`, the factory calls are
// wrapped in thunks to prevent orphan objects on bars 1+. Here on bar 0,
// we evaluate those thunks to get the actual drawing objects.
if (src instanceof PineTypeObject) {
const def = src.__def__;
for (const key in def) {
if (typeof src[key] === 'function') {
src[key] = src[key]();
}
}
}

// First bar: Initialize with source value
let value;
if (src instanceof Series) {
Expand Down
3 changes: 2 additions & 1 deletion src/namespaces/array/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export function inferValueType(value: any): PineArrayType {
} else if (typeof value === 'boolean') {
return PineArrayType.bool;
} else {
throw new Error('Cannot infer type from value');
// Objects (LineObject, LabelObject, BoxObject, etc.) get 'any' type
return PineArrayType.any;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/namespaces/box/BoxHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class BoxHelper {
this._resolve(text_formatting) || 'format_none',
force_overlay,
);
b._helper = this;
this._boxes.push(b);
this._syncToPlot();
return b;
Expand Down Expand Up @@ -289,6 +290,7 @@ export class BoxHelper {
copy(id: BoxObject): BoxObject | undefined {
if (!id) return undefined;
const b = id.copy();
b._helper = this;
this._boxes.push(b);
this._syncToPlot();
return b;
Expand Down
32 changes: 32 additions & 0 deletions src/namespaces/box/BoxObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class BoxObject {
// Flags
public force_overlay: boolean;
public _deleted: boolean;
public _helper: any;

constructor(
left: number,
Expand Down Expand Up @@ -77,8 +78,39 @@ export class BoxObject {
this.text_formatting = text_formatting;
this.force_overlay = force_overlay;
this._deleted = false;
this._helper = null;
}

// --- Delegate methods for method-call syntax (e.g. myBox.set_right(x)) ---

set_left(left: number): void { if (this._helper) this._helper.set_left(this, left); else if (!this._deleted) this.left = left; }
set_right(right: number): void { if (this._helper) this._helper.set_right(this, right); else if (!this._deleted) this.right = right; }
set_top(top: number): void { if (this._helper) this._helper.set_top(this, top); else if (!this._deleted) this.top = top; }
set_bottom(bottom: number): void { if (this._helper) this._helper.set_bottom(this, bottom); else if (!this._deleted) this.bottom = bottom; }
set_lefttop(left: number, top: number): void { if (this._helper) this._helper.set_lefttop(this, left, top); else if (!this._deleted) { this.left = left; this.top = top; } }
set_rightbottom(right: number, bottom: number): void { if (this._helper) this._helper.set_rightbottom(this, right, bottom); else if (!this._deleted) { this.right = right; this.bottom = bottom; } }
set_top_left_point(point: any): void { if (this._helper) this._helper.set_top_left_point(this, point); }
set_bottom_right_point(point: any): void { if (this._helper) this._helper.set_bottom_right_point(this, point); }
set_xloc(left: number, right: number, xloc: string): void { if (this._helper) this._helper.set_xloc(this, left, right, xloc); else if (!this._deleted) { this.left = left; this.right = right; this.xloc = xloc; } }
set_bgcolor(color: string): void { if (this._helper) this._helper.set_bgcolor(this, color); else if (!this._deleted) this.bgcolor = color; }
set_border_color(color: string): void { if (this._helper) this._helper.set_border_color(this, color); else if (!this._deleted) this.border_color = color; }
set_border_width(width: number): void { if (this._helper) this._helper.set_border_width(this, width); else if (!this._deleted) this.border_width = width; }
set_border_style(style: string): void { if (this._helper) this._helper.set_border_style(this, style); else if (!this._deleted) this.border_style = style; }
set_extend(extend: string): void { if (this._helper) this._helper.set_extend(this, extend); else if (!this._deleted) this.extend = extend; }
set_text(text: string): void { if (this._helper) this._helper.set_text(this, text); else if (!this._deleted) this.text = text; }
set_text_color(color: string): void { if (this._helper) this._helper.set_text_color(this, color); else if (!this._deleted) this.text_color = color; }
set_text_size(size: string): void { if (this._helper) this._helper.set_text_size(this, size); else if (!this._deleted) this.text_size = size; }
set_text_halign(align: string): void { if (this._helper) this._helper.set_text_halign(this, align); else if (!this._deleted) this.text_halign = align; }
set_text_valign(align: string): void { if (this._helper) this._helper.set_text_valign(this, align); else if (!this._deleted) this.text_valign = align; }
set_text_wrap(wrap: string): void { if (this._helper) this._helper.set_text_wrap(this, wrap); else if (!this._deleted) this.text_wrap = wrap; }
set_text_font_family(family: string): void { if (this._helper) this._helper.set_text_font_family(this, family); else if (!this._deleted) this.text_font_family = family; }
set_text_formatting(formatting: string): void { if (this._helper) this._helper.set_text_formatting(this, formatting); else if (!this._deleted) this.text_formatting = formatting; }

get_left(): number { return this.left; }
get_right(): number { return this.right; }
get_top(): number { return this.top; }
get_bottom(): number { return this.bottom; }

delete(): void {
this._deleted = true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/namespaces/label/LabelHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export class LabelHelper {
this._resolve(text_font_family),
force_overlay,
);
lbl._helper = this;
this._labels.push(lbl);
this._syncToPlot();
return lbl;
Expand Down Expand Up @@ -235,6 +236,7 @@ export class LabelHelper {
copy(id: LabelObject): LabelObject | undefined {
if (!id) return undefined;
const lbl = id.copy();
lbl._helper = this;
this._labels.push(lbl);
this._syncToPlot();
return lbl;
Expand Down
23 changes: 23 additions & 0 deletions src/namespaces/label/LabelObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class LabelObject {
public text_font_family: string;
public force_overlay: boolean;
public _deleted: boolean;
public _helper: any;

constructor(
x: number,
Expand Down Expand Up @@ -53,8 +54,30 @@ export class LabelObject {
this.text_font_family = text_font_family;
this.force_overlay = force_overlay;
this._deleted = false;
this._helper = null;
}

// --- Delegate methods for method-call syntax (e.g. myLabel.set_x(x)) ---

set_x(x: number): void { if (this._helper) this._helper.set_x(this, x); else if (!this._deleted) this.x = x; }
set_y(y: number): void { if (this._helper) this._helper.set_y(this, y); else if (!this._deleted) this.y = y; }
set_xy(x: number, y: number): void { if (this._helper) this._helper.set_xy(this, x, y); else if (!this._deleted) { this.x = x; this.y = y; } }
set_text(text: string): void { if (this._helper) this._helper.set_text(this, text); else if (!this._deleted) this.text = text; }
set_color(color: string): void { if (this._helper) this._helper.set_color(this, color); else if (!this._deleted) this.color = color; }
set_textcolor(textcolor: string): void { if (this._helper) this._helper.set_textcolor(this, textcolor); else if (!this._deleted) this.textcolor = textcolor; }
set_size(size: string): void { if (this._helper) this._helper.set_size(this, size); else if (!this._deleted) this.size = size; }
set_style(style: string): void { if (this._helper) this._helper.set_style(this, style); else if (!this._deleted) this.style = style; }
set_textalign(textalign: string): void { if (this._helper) this._helper.set_textalign(this, textalign); else if (!this._deleted) this.textalign = textalign; }
set_tooltip(tooltip: string): void { if (this._helper) this._helper.set_tooltip(this, tooltip); else if (!this._deleted) this.tooltip = tooltip; }
set_xloc(xloc: string): void { if (this._helper) this._helper.set_xloc(this, xloc); else if (!this._deleted) this.xloc = xloc; }
set_yloc(yloc: string): void { if (this._helper) this._helper.set_yloc(this, yloc); else if (!this._deleted) this.yloc = yloc; }
set_point(point: any): void { if (this._helper) this._helper.set_point(this, point); }
set_text_font_family(family: string): void { if (!this._deleted) this.text_font_family = family; }

get_x(): number { return this.x; }
get_y(): number { return this.y; }
get_text(): string { return this.text; }

delete(): void {
this._deleted = true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/namespaces/line/LineHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class LineHelper {
this._resolve(width) || 1,
force_overlay,
);
ln._helper = this;
this._lines.push(ln);
this._syncToPlot();
return ln;
Expand Down Expand Up @@ -260,6 +261,7 @@ export class LineHelper {
copy(id: LineObject): LineObject | undefined {
if (!id) return undefined;
const ln = id.copy();
ln._helper = this;
this._lines.push(ln);
this._syncToPlot();
return ln;
Expand Down
24 changes: 24 additions & 0 deletions src/namespaces/line/LineObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class LineObject {
public width: number;
public force_overlay: boolean;
public _deleted: boolean;
public _helper: any;

constructor(
x1: number,
Expand All @@ -44,8 +45,31 @@ export class LineObject {
this.width = width;
this.force_overlay = force_overlay;
this._deleted = false;
this._helper = null;
}

// --- Delegate methods for method-call syntax (e.g. myLine.set_x2(x)) ---

set_x1(x: number): void { if (this._helper) this._helper.set_x1(this, x); else if (!this._deleted) this.x1 = x; }
set_y1(y: number): void { if (this._helper) this._helper.set_y1(this, y); else if (!this._deleted) this.y1 = y; }
set_x2(x: number): void { if (this._helper) this._helper.set_x2(this, x); else if (!this._deleted) this.x2 = x; }
set_y2(y: number): void { if (this._helper) this._helper.set_y2(this, y); else if (!this._deleted) this.y2 = y; }
set_xy1(x: number, y: number): void { if (this._helper) this._helper.set_xy1(this, x, y); else if (!this._deleted) { this.x1 = x; this.y1 = y; } }
set_xy2(x: number, y: number): void { if (this._helper) this._helper.set_xy2(this, x, y); else if (!this._deleted) { this.x2 = x; this.y2 = y; } }
set_color(color: string): void { if (this._helper) this._helper.set_color(this, color); else if (!this._deleted) this.color = color; }
set_width(width: number): void { if (this._helper) this._helper.set_width(this, width); else if (!this._deleted) this.width = width; }
set_style(style: string): void { if (this._helper) this._helper.set_style(this, style); else if (!this._deleted) this.style = style; }
set_extend(extend: string): void { if (this._helper) this._helper.set_extend(this, extend); else if (!this._deleted) this.extend = extend; }
set_xloc(x1: number, x2: number, xloc: string): void { if (this._helper) this._helper.set_xloc(this, x1, x2, xloc); else if (!this._deleted) { this.x1 = x1; this.x2 = x2; this.xloc = xloc; } }
set_first_point(point: any): void { if (this._helper) this._helper.set_first_point(this, point); }
set_second_point(point: any): void { if (this._helper) this._helper.set_second_point(this, point); }

get_x1(): number { return this.x1; }
get_y1(): number { return this.y1; }
get_x2(): number { return this.x2; }
get_y2(): number { return this.y2; }
get_price(x: number): number { if (this._helper) return this._helper.get_price(this, x); return NaN; }

delete(): void {
this._deleted = true;
}
Expand Down
Loading