diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 25b5d0ad..6df604a6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,6 +33,9 @@ jobs: - name: Build library for browser run: bun build ./src/index.ts --outdir ./playground/dist --target browser --minify + - name: Bundle TypeScript compiler for offline playground + run: cp node_modules/typescript/lib/typescript.js ./playground/dist/typescript.js + - name: Setup Pages uses: actions/configure-pages@v5 with: diff --git a/biome.json b/biome.json index f98946cc..5e64d5ce 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "ignore": ["dist/**", "node_modules/**", "*.d.ts", "playground/**/*.js"] + "ignore": ["dist/**", "node_modules/**", "*.d.ts", "playground/**/*.js", "playground/serve.ts"] }, "formatter": { "enabled": true, diff --git a/docs/playground.md b/docs/playground.md index 2fa3fed0..b2e64183 100644 --- a/docs/playground.md +++ b/docs/playground.md @@ -24,7 +24,8 @@ compiler from CDN and transpiles user code to JavaScript before execution. Users can write type annotations, interfaces, and generics — the compiler strips them automatically. -- TypeScript compiler: loaded from `https://cdn.jsdelivr.net/npm/typescript@5/lib/typescript.js` +- TypeScript compiler: bundled locally (`playground/dist/typescript.js`) with + CDN fallback (`https://cdn.jsdelivr.net/npm/typescript@5/lib/typescript.js`). - No WASM required — the compiler runs natively in JavaScript. ### 3. Live tsb Library Access @@ -112,6 +113,7 @@ playground/ ```bash bun build ./src/index.ts --outdir ./playground/dist --target browser --minify +cp node_modules/typescript/lib/typescript.js ./playground/dist/typescript.js ``` The CI pipeline (`pages.yml`) runs this automatically during deployment. diff --git a/playground/concat.html b/playground/concat.html index 7ffea211..fd19e773 100644 --- a/playground/concat.html +++ b/playground/concat.html @@ -3,7 +3,7 @@ - tsb — concat Tutorial + tsb — concat Playground -
-

tsb

- ← All features - concat -
- -
-

- concat(objs, options?) combines Series or DataFrames along either axis. - It mirrors pandas.concat. -

- - -

1 · Stack Series vertically axis=0

-

The default axis stacks rows. Index labels are preserved and concatenated.

-
-
TypeScript
-
import { Series, concat } from "tsb";
-
-const s1 = new Series({ data: [10, 20], index: ["a", "b"] });
-const s2 = new Series({ data: [30, 40], index: ["c", "d"] });
-
-concat([s1, s2]);
-
-
Output — Series, length 4
- - - - - - - - -
(index)value
a10
b20
c30
d40
-
+ +
+
+
Initializing playground…
- -

2 · Stack DataFrames vertically — outer join axis=0

+ ← Back to roadmap +

🔗 concat — Interactive Playground

- When DataFrames have different columns, join="outer" (default) fills gaps with - null. + concat(objs, options?) combines Series or DataFrames along + either axis — mirroring + pandas.concat.
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser.

-
-
TypeScript
-
import { DataFrame, concat } from "tsb";
-
-const df1 = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
-const df2 = DataFrame.fromColumns({ b: [5], c: [6] });
-
-concat([df1, df2]);              // join="outer" by default
-
-
Output — 3 rows × 3 cols (null fills missing values)
- - - - - - - -
(index)abc
013null
124null
2null56
+ + +
+

1 · Stack Series vertically (axis=0)

+

The default axis stacks rows. Index labels are preserved and concatenated.

+
+
+ TypeScript +
+ + +
+
+
import { Series, concat } from "tsb";
+
+const s1 = new Series({ data: [10, 20], index: ["a", "b"] });
+const s2 = new Series({ data: [30, 40], index: ["c", "d"] });
+
+const result = concat([s1, s2]);
+console.log(result.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

3 · Stack DataFrames vertically — inner join axis=0

-

- Pass join="inner" to keep only columns shared by all DataFrames. -

-
-
TypeScript
-
concat([df1, df2], { join: "inner" });
-
-
Output — 3 rows × 1 col (only "b" is common)
- - - - - - - -
(index)b
03
14
25
+ +
+

2 · Stack DataFrames vertically (axis=0)

+

When DataFrames share the same columns, rows are stacked with the default join="outer". Missing columns are filled with null.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, concat } from "tsb";
+
+const df1 = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
+const df2 = DataFrame.fromColumns({ b: [5], c: [6] });
+
+// join="outer" by default — fills missing columns with null
+const result = concat([df1, df2]);
+console.log(result.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

4 · Reset the index

-

- Set ignoreIndex: true to discard incoming labels and get a clean - RangeIndex. -

-
-
TypeScript
-
const a = new Series({ data: [1, 2], index: ["x", "y"] });
-const b = new Series({ data: [3], index: ["z"] });
-
-concat([a, b], { ignoreIndex: true });
-
-
Output — index is 0, 1, 2
- - - - - - - -
(index)value
01
12
23
+ +
+

3 · Column-wise concat (axis=1)

+

With axis: 1, each Series becomes a column of the result DataFrame. + The Series name is used as the column label. + DataFrames are merged side by side.

+
+
+ TypeScript +
+ + +
+
+
import { Series, DataFrame, concat } from "tsb";
+
+// Series → DataFrame (each Series becomes a column)
+const age   = new Series({ data: [25, 30, 35], name: "age" });
+const score = new Series({ data: [88, 92, 79], name: "score" });
+
+console.log("=== Series axis=1 ===");
+console.log(concat([age, score], { axis: 1 }).toString());
+
+// DataFrame side-by-side
+const left  = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
+const right = DataFrame.fromColumns({ c: [5, 6], d: [7, 8] });
+
+console.log("\n=== DataFrame axis=1 ===");
+console.log(concat([left, right], { axis: 1 }).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

5 · Combine Series as columns axis=1

-

- With axis=1, each Series becomes a column of the result DataFrame. - Series name is used as the column label. -

-
-
TypeScript
-
const age  = new Series({ data: [25, 30, 35], name: "age" });
-const score = new Series({ data: [88, 92, 79], name: "score" });
-
-concat([age, score], { axis: 1 });
-
-
Output — DataFrame 3 × 2
- - - - - - - -
(index)agescore
02588
13092
23579
+ +
+

4 · Join modes — outer vs inner

+

+ join="outer" (default) keeps the union of labels and fills gaps with null. + join="inner" keeps only the intersection. +

+
+
+ TypeScript +
+ + +
+
+
import { Series, DataFrame, concat } from "tsb";
+
+// axis=0: outer vs inner columns
+const df1 = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
+const df2 = DataFrame.fromColumns({ b: [5], c: [6] });
+
+console.log("=== axis=0, join='outer' (default) ===");
+console.log(concat([df1, df2]).toString());
+
+console.log("\n=== axis=0, join='inner' (only shared col 'b') ===");
+console.log(concat([df1, df2], { join: "inner" }).toString());
+
+// axis=1: outer vs inner row indexes
+const s1 = new Series({ data: [1, 2], index: ["a", "b"], name: "s1" });
+const s2 = new Series({ data: [3, 4], index: ["b", "c"], name: "s2" });
+
+console.log("\n=== axis=1, join='outer' (union of row indexes) ===");
+console.log(concat([s1, s2], { axis: 1 }).toString());
+
+console.log("\n=== axis=1, join='inner' (only shared row 'b') ===");
+console.log(concat([s1, s2], { axis: 1, join: "inner" }).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

6 · Merge DataFrames side by side axis=1

-

- With axis=1, DataFrames are merged column-wise; rows are aligned on - the row index. -

-
-
TypeScript
-
const left  = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
-const right = DataFrame.fromColumns({ c: [5, 6], d: [7, 8] });
-
-concat([left, right], { axis: 1 });
-
-
Output — DataFrame 2 × 4
- - - - - - -
(index)abcd
01357
12468
+ +
+

5 · ignoreIndex — reset to RangeIndex

+

Set ignoreIndex: true to discard incoming labels and get a clean 0, 1, 2, … index.

+
+
+ TypeScript +
+ + +
+
+
import { Series, DataFrame, concat } from "tsb";
+
+// Series with string indexes → reset to 0, 1, 2
+const a = new Series({ data: [1, 2], index: ["x", "y"] });
+const b = new Series({ data: [3], index: ["z"] });
+
+console.log("=== Series ignoreIndex ===");
+console.log(concat([a, b], { ignoreIndex: true }).toString());
+
+// DataFrame ignoreIndex
+const df1 = DataFrame.fromColumns({ v: [10, 20] });
+const df2 = DataFrame.fromColumns({ v: [30, 40] });
+
+console.log("\n=== DataFrame ignoreIndex ===");
+console.log(concat([df1, df2], { ignoreIndex: true }).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

7 · axis=1 with unaligned row indexes

-

- When Series (or DataFrames) have different row-indexes on axis=1, - the outer join fills missing values with null. -

-
-
TypeScript
-
const s1 = new Series({ data: [1, 2], index: ["a", "b"], name: "s1" });
-const s2 = new Series({ data: [3, 4], index: ["b", "c"], name: "s2" });
-
-concat([s1, s2], { axis: 1 });  // outer (default)
-
-
Output — 3 rows (union of "a","b","c")
- - - - - - - -
(index)s1s2
a1null
b23
cnull4
+ +
+

🧪 Scratch Pad

+

Write your own concat code below. All exports from tsb are available: + DataFrame, Series, Index, concat, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+
import { DataFrame, Series, concat } from "tsb";
+
+// Try it! Combine DataFrames in creative ways.
+const q1 = DataFrame.fromColumns({
+  product:  ["Widget", "Gadget"],
+  revenue:  [1000, 1500],
+});
+
+const q2 = DataFrame.fromColumns({
+  product:  ["Widget", "Gadget"],
+  revenue:  [1200, 1800],
+});
+
+console.log("=== Q1 + Q2 stacked ===");
+console.log(concat([q1, q2], { ignoreIndex: true }).toString());
+
+// Side-by-side with axis=1
+const names = new Series({ data: ["Widget", "Gadget"], name: "product" });
+const q1rev = new Series({ data: [1000, 1500], name: "q1_rev" });
+const q2rev = new Series({ data: [1200, 1800], name: "q2_rev" });
+
+console.log("\n=== Side-by-side columns ===");
+console.log(concat([names, q1rev, q2rev], { axis: 1 }).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

API Reference

- - - - - - - - - -
OptionTypeDefaultDescription
axis0 | 1 | "index" | "columns"0Stack rows (0) or columns (1)
join"outer" | "inner""outer"Union or intersection of labels on the other axis
ignoreIndexbooleanfalseReset the concatenation axis to a RangeIndex
- -

Supported combinations

- - - - - - - - -
Input typeaxis=0 resultaxis=1 result
Series[]SeriesDataFrame
DataFrame[]DataFrameDataFrame
- -
- - + + + diff --git a/playground/corr.html b/playground/corr.html index 10d612b5..e5c918cf 100644 --- a/playground/corr.html +++ b/playground/corr.html @@ -1,73 +1,273 @@ - - - - tsb — corr & cov - - - - - -

corr & cov — Pearson Correlation & Covariance

-

- DataFrame.corr(), DataFrame.cov(), and - Series.corr() mirror - pandas DataFrame.corr() - and - pandas DataFrame.cov(). - The standalone pearsonCorr(a, b) and - dataFrameCorr(df) / dataFrameCov(df) functions - provide the same functionality. + + + + tsb — corr & cov Playground + + + + + +

+
+
Initializing playground…
+
+ + ← Back to roadmap +

📊 corr & cov — Interactive Playground

+

+ pearsonCorr(a, b) computes the Pearson correlation between two + Series. dataFrameCorr(df) and dataFrameCov(df) + produce symmetric N×N correlation and covariance matrices — mirroring + pandas.DataFrame.corr() and + pandas.DataFrame.cov().
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

1 · Series pearsonCorr

+

+ pearsonCorr(a, b) computes the Pearson correlation coefficient + between two Series, aligning on shared index labels and ignoring missing + values. Returns a number in [−1, 1], or NaN when a valid + result cannot be computed.

+
+
+ TypeScript +
+ + +
+
+
import { Series, pearsonCorr } from "tsb";
+
+const temperature = new Series({ data: [22, 24, 28, 31, 35], name: "temp_C" });
+const ice_cream   = new Series({ data: [120, 145, 190, 230, 285], name: "sales" });
 
-    
-

1 — Series.corr() — Pearson correlation between two Series

-

- a.corr(b) computes the Pearson correlation coefficient - between Series a and b, aligning on shared index - labels and ignoring missing values. Returns a number in [−1, 1], or - NaN when a valid result cannot be computed. -

-
-
const temperature = new tsb.Series({ data: [22, 24, 28, 31, 35], name: "temp_C" });
-const ice_cream   = new tsb.Series({ data: [120, 145, 190, 230, 285], name: "sales" });
-
-const r = temperature.corr(ice_cream);
+const r = pearsonCorr(temperature, ice_cream);
 console.log("Pearson r:", r.toFixed(4));   // strong positive correlation
 
 // Negative correlation
-const warm_clothes = new tsb.Series({ data: [310, 280, 210, 150, 90], name: "jackets" });
-console.log("r (temp vs jackets):", temperature.corr(warm_clothes).toFixed(4));
-
+const warm_clothes = new Series({ data: [310, 280, 210, 150, 90], name: "jackets" }); +console.log("r (temp vs jackets):", pearsonCorr(temperature, warm_clothes).toFixed(4)); + +// Missing values are dropped per-pair +const c = new Series({ data: [1, null, 3, 4] }); +const d = new Series({ data: [2, 4, 6, 8] }); +console.log("r (nulls dropped):", pearsonCorr(c, d).toFixed(4)); + +// Require at least 5 valid pairs — returns NaN when fewer exist +console.log("r (minPeriods=5):", pearsonCorr(c, d, { minPeriods: 5 }));
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

2 · DataFrame corr matrix (dataFrameCorr)

+

+ dataFrameCorr(df) returns a symmetric N×N DataFrame where + entry [i, j] is the Pearson correlation between columns i and + j. Diagonal entries are always 1. Only numeric columns are + included; string columns are silently skipped. +

+
+
+ TypeScript +
+ + +
- -

-    
-
-    
-

2 — DataFrame.corr() — pairwise correlation matrix

-

- df.corr() returns a symmetric N×N DataFrame where entry - [i, j] is the Pearson correlation between columns i and - j. Diagonal entries are always 1. Only numeric columns are - included; string columns are silently skipped. -

-
-
const df = tsb.DataFrame.fromColumns({
+      
import { DataFrame, dataFrameCorr } from "tsb";
+
+const df = DataFrame.fromColumns({
   height: [160, 172, 185, 155, 168],
   weight: [55,   72,  90,  48,  65],
   age:    [22,   35,  28,  19,  31],
   city:   ["A", "B", "C", "A", "B"],  // non-numeric — skipped
 });
 
-const r = df.corr();
+const r = dataFrameCorr(df);
 console.log("columns:", [...r.columns.values]);
 console.log("shape:", r.shape);
 
@@ -76,28 +276,37 @@ 

2 — DataFrame.corr() — pairwise correlation matrix

const rHA = r.col("age").iat(0); // corr(height, age) console.log("height–weight r:", rHW.toFixed(4)); console.log("height–age r: ", rHA.toFixed(4)); -console.log("diagonal:", [r.col("height").iat(0), r.col("weight").iat(1), r.col("age").iat(2)]); -
+console.log("diagonal:", [r.col("height").iat(0), r.col("weight").iat(1), r.col("age").iat(2)]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

3 · DataFrame cov matrix (dataFrameCov)

+

+ dataFrameCov(df) returns the sample covariance matrix + (denominator n − 1). Pass { ddof: 0 } for + population covariance. Diagonal entries are the variance of each column. +

+
+
+ TypeScript +
+ + +
- -

-    
-
-    
-

3 — DataFrame.cov() — pairwise covariance matrix

-

- df.cov() returns the sample covariance matrix (denominator - n − 1). Pass ddof=0 for population covariance. - Diagonal entries are the variance of each column. -

-
-
const returns = tsb.DataFrame.fromColumns({
+      
import { DataFrame, dataFrameCov } from "tsb";
+
+const returns = DataFrame.fromColumns({
   AAPL: [ 0.02,  0.01, -0.03,  0.04,  0.01],
   GOOG: [ 0.01,  0.02, -0.02,  0.03,  0.02],
   TSLA: [-0.05,  0.08, -0.10,  0.12, -0.03],
 });
 
-const cov = returns.cov();
+const cov = dataFrameCov(returns);
 console.log("Covariance matrix:");
 for (const col of cov.columns.values) {
   const vals = [...cov.col(col).values].map(v => (v).toFixed(6));
@@ -105,70 +314,67 @@ 

3 — DataFrame.cov() — pairwise covariance matrix

} // Compare sample (ddof=1) vs population (ddof=0) variance -const varAAPL_sample = returns.cov(1).col("AAPL").iat(0); -const varAAPL_pop = returns.cov(0).col("AAPL").iat(0); -console.log("\nAAPL sample variance:", varAAPL_sample.toFixed(6)); -console.log("AAPL population variance:", varAAPL_pop.toFixed(6)); -
+const varSample = dataFrameCov(returns, { ddof: 1 }).col("AAPL").iat(0); +const varPop = dataFrameCov(returns, { ddof: 0 }).col("AAPL").iat(0); +console.log("\nAAPL sample variance:", varSample.toFixed(6)); +console.log("AAPL population variance:", varPop.toFixed(6));
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

🧪 Scratch Pad

+

Write your own corr & cov code below. All exports from tsb are available: + DataFrame, Series, pearsonCorr, + dataFrameCorr, dataFrameCov, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
- -

-    
-
-    
-

4 — Handling missing values and index alignment

-

- All functions align on shared index labels (inner join) and silently - drop pairs where either value is null or - NaN. Use minPeriods to require a minimum - number of valid observation pairs. -

-
-
// Different indices — only shared labels are used
-const a = new tsb.Series({ data: [1, 2, 3, 4], index: ["w", "x", "y", "z"] });
-const b = new tsb.Series({ data: [5, 6, 7, 8], index: ["x", "y", "z", "q"] });
-// shared: x(2,5), y(3,6), z(4,7)
-console.log("corr (inner join):", tsb.pearsonCorr(a, b).toFixed(4));  // 1.0
+      
import { DataFrame, Series, pearsonCorr, dataFrameCorr, dataFrameCov } from "tsb";
 
-// Missing values are dropped per-pair
-const c = new tsb.Series({ data: [1, null, 3, 4] });
-const d = new tsb.Series({ data: [2,    4, 6, 8] });
-// null at position 1 is dropped; pairs (1,2),(3,6),(4,8) remain
-console.log("corr (nulls dropped):", tsb.pearsonCorr(c, d).toFixed(4));  // 1.0
+// Try it! Explore correlation and covariance.
+const a = new Series({ data: [1, 2, 3, 4, 5] });
+const b = new Series({ data: [2, 4, 6, 8, 10] });
 
-// Require at least 5 valid pairs — returns NaN when fewer exist
-console.log("corr (minPeriods=5):", tsb.pearsonCorr(c, d, { minPeriods: 5 }));  // NaN
-
-
- -

-    
- - - +console.log("Perfect positive r:", pearsonCorr(a, b)); + +const df = DataFrame.fromColumns({ + x: [1, 2, 3, 4, 5], + y: [5, 4, 3, 2, 1], + z: [2, 4, 6, 8, 10], +}); + +console.log("\nCorrelation matrix:"); +for (const col of dataFrameCorr(df).columns.values) { + const vals = [...dataFrameCorr(df).col(col).values].map(v => (v).toFixed(2)); + console.log(col + ":", vals.join(" ")); +} + +console.log("\nCovariance matrix:"); +for (const col of dataFrameCov(df).columns.values) { + const vals = [...dataFrameCov(df).col(col).values].map(v => (v).toFixed(2)); + console.log(col + ":", vals.join(" ")); +} +
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + + + + + diff --git a/playground/dataframe.html b/playground/dataframe.html index 5b21a9a5..e0b50697 100644 --- a/playground/dataframe.html +++ b/playground/dataframe.html @@ -3,7 +3,7 @@ - tsb — DataFrame Tutorial + tsb — DataFrame Playground -
-
-

🗃️ DataFrame

+ + +
+
+
Initializing playground…
- ← Back to tsb Playground -
-
+ ← Back to roadmap +

🗃️ DataFrame — Interactive Playground

- DataFrame is the heart of tsb: a two-dimensional, column-oriented - table where every column is a typed Series. It mirrors pandas.DataFrame - with a fully strict TypeScript API. + DataFrame is the heart of tsb: a + two-dimensional, column-oriented table where every column is a typed + Series. It mirrors pandas.DataFrame with a fully + strict TypeScript API.
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser.

- -

Construction

-

Three factory methods cover the most common shapes of input data.

+ +
+

Construction

+

Three factory methods cover the most common shapes of input data: + fromColumns, fromRecords, and from2D.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
-
import { DataFrame } from "tsb";
+  
-const df = DataFrame.fromColumns({ - name: ["Alice", "Bob", "Carol"], - age: [30, 25, 35], - score: [90, 80, 95], + +
+

Properties

+

Inspect the shape, dimensionality, size, and axes of a DataFrame.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
-
const df = DataFrame.fromRecords([
-  { city: "London", pop: 9_000_000 },
-  { city: "Tokyo",  pop: 14_000_000 },
-  { city: "NYC",    pop: 8_300_000 },
-]);
-
-df.shape;  // [3, 2]
-
-
- from2D - matrix -
-
const df = DataFrame.from2D(
-  [[1, 4], [2, 5], [3, 6]],
-  ["a", "b"],
-);
+  
+  
+

Column Access

+

Retrieve columns with col() (throws if missing), + get() (returns undefined), and check existence with has().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Column Access

+ +
+

Slicing

+

Select rows by position with head(), tail(), + iloc(), or by label with loc().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Column Mutations

-
All mutation methods return a new DataFrame — tsb is immutable.
+ +
+

Column Mutations

+

All mutation methods return a new + DataFrame — tsb is immutable. Use assign(), drop(), + select(), and rename().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
-// Remove columns -const df3 = df.drop(["score"]); + +
+

Missing Values

+

Detect, drop, and fill null values across the entire DataFrame.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Aggregations

- -
-
sum / mean / min / max / std / count / describe
-
const df = DataFrame.fromColumns({
-  a: [10, 20, 30],
-  b: [1, 2, 3],
+  
+  
+

Aggregations

+

Column-wise aggregates: sum(), mean(), + min(), max(), count(), and the + all-in-one describe().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Sorting

- -
-
sortValues / sortIndex
-
const df = DataFrame.fromColumns({
-  name:  ["Alice", "Bob", "Carol"],
-  score: [90, 80, 95],
+  
+  
+

Sorting

+

Sort rows by column values with sortValues() or by the + index with sortIndex().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Apply

- -
-
apply(fn, axis)
-
const df = DataFrame.fromColumns({ a: [1, 2, 3], b: [4, 5, 6] });
-
-// axis=0: apply to each column
-df.apply((s) => s.sum(), 0);
-// Series { a: 6, b: 15 }
+  
+  
+

Apply & Iteration

+

Use apply() to run a function over columns (axis 0) or rows + (axis 1). Iterate with items() and iterrows().

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Conversion

- -
-
toRecords / toDict / toArray
-
const df = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] });
-
-df.toRecords();
-// [{ a: 1, b: 3 }, { a: 2, b: 4 }]
-
-df.toDict();
-// { a: [1, 2], b: [3, 4] }
+  
+  
+

Conversion

+

Convert between DataFrames and plain JavaScript structures. + Use setIndex() and resetIndex() to manipulate + the row index.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
- -

Index Manipulation

- -
-
setIndex / resetIndex
-
const df = DataFrame.fromColumns({
-  name:  ["Alice", "Bob"],
-  score: [90, 80],
-});
-
-// Promote "name" to the row index
-const indexed = df.setIndex("name");
-indexed.index.values;  // ["Alice", "Bob"]
-indexed.columns.values; // ["score"]
+  
+  
+

🧪 Try It Yourself

+

Write your own tsb code below. All exports from tsb are available: + DataFrame, Series, Index, Dtype, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
-

- tsb is a TypeScript port of pandas, built from first principles. - — ← Back to Playground -

+ -
+ + diff --git a/playground/dtype.html b/playground/dtype.html new file mode 100644 index 00000000..5381c603 --- /dev/null +++ b/playground/dtype.html @@ -0,0 +1,470 @@ + + + + + + tsb — Dtype Playground + + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

🔢 Dtype — Interactive Playground

+

+ The Dtype class is tsb's immutable, singleton type descriptor + — mirroring pandas' dtype hierarchy with 16 built-in types covering + integers, floats, booleans, strings, datetimes, and more.
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

Creating Dtypes

+

Obtain dtype instances via Dtype.from() or use the static singletons. + Identity comparison (===) works because every dtype is a cached singleton.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Kind Classification

+

Each dtype exposes boolean predicates for its classification — + isNumeric, isInteger, isFloat, and more.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Item Sizes

+

The itemsize property returns the byte width of each element. + Variable-length types (string, object, category) return 0.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Type Casting

+

Use canCastTo() to check safe promotion rules — whether values + of one dtype can be losslessly represented in another.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Common Type Resolution

+

Dtype.commonType() finds the smallest dtype that can represent + both inputs without loss. Falls back to object when no numeric + promotion exists.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Type Inference

+

Dtype.inferFrom() auto-detects the most specific dtype from + an array of values — booleans, integers, floats, dates, strings, or mixed.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

🧪 Try It Yourself

+

Write your own tsb code below. All exports from tsb are available: + Dtype, Series, Index, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + + + + + + diff --git a/playground/groupby.html b/playground/groupby.html index 9edf578e..4a8b5e80 100644 --- a/playground/groupby.html +++ b/playground/groupby.html @@ -3,7 +3,7 @@ - tsb — GroupBy Tutorial + tsb — GroupBy Playground -
-

tsb

- ← All features - - GroupBy -
- -
+ +
+
+
Initializing playground…
+
-

GroupBy: Split–Apply–Combine

+ ← Back to roadmap +

🔀 GroupBy — Interactive Playground

- The GroupBy engine lets you split a DataFrame (or Series) into groups, apply an aggregation - or transformation to each group, and combine the results — mirroring - pandas.DataFrame.groupby(). + The GroupBy engine lets you split a DataFrame (or Series) + into groups, apply an aggregation or transformation to each group, and + combine the results — mirroring + pandas.DataFrame.groupby().
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser.

- -

1 · Basic groupby + sum

-

Group by a single column and aggregate with a built-in function:

-
-
TypeScript
-
import { DataFrame } from "tsb";
-
-const df = DataFrame.fromColumns({
-  dept:  ["A", "A", "B", "B", "C"],
-  sales: [10, 20, 30, 40, 50],
-  bonus: [1,  2,  3,  4,  5 ],
+  
+  
+

1 · Basic groupby + sum()

+

Group by a single column and aggregate with a built-in function. sum() only includes numeric columns.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  5],
 });
 
-const result = df.groupby("dept").sum();
-console.log(result.toString());
-
-
Output
- - - - - - - -
(index)salesbonus
A303
B707
C505
+const result = df.groupby("dept").sum(); +console.log(result.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

2 · Per-column aggregation specs

-

Apply different aggregation functions to different columns using an object spec:

-
-
TypeScript
-
df.groupby("dept").agg({
-  sales: "sum",
-  bonus: "mean",
-});
-
-
Output
- - - - - - - -
(index)salesbonus
A301.5
B703.5
C505
+ +
+

2 · mean(), min(), max()

+

Built-in aggregation shorthands. mean() only includes numeric columns; min()/max() work on all value columns.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  team:   ["X", "X", "Y", "Y", "Z"],
+  points: [10, 20, 30, 40, 50],
+  fouls:  [2,  4,  1,  3,  5],
+});
+
+console.log("=== mean() ===");
+console.log(df.groupby("team").mean().toString());
+
+console.log("\n=== min() ===");
+console.log(df.groupby("team").min().toString());
+
+console.log("\n=== max() ===");
+console.log(df.groupby("team").max().toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

3 · Custom aggregation function

-

Pass any function (vals: readonly Scalar[]) => Scalar:

-
-
TypeScript
-
// Range = max − min per group
-df.groupby("dept").agg((vals) => {
-  const nums = vals.filter((v): v is number => typeof v === "number");
-  if (nums.length === 0) return 0;
-  return Math.max(...nums) - Math.min(...nums);
-});
-
-
Output (sales range)
- - - - - - - -
(index)salesbonus
A101
B101
C00
+ +
+

3 · count()

+

Count non-null values per group and column. Missing values are excluded from the count.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:   ["A", "A", "B", "B", "B"],
+  score:  [90,  null, 80, 70, null],
+  rating: [5,   4,    null, 3, 2],
+});
+
+const counts = df.groupby("dept").count();
+console.log(counts.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

4 · transform()

-

- Unlike agg(), transform() returns a same-shape DataFrame — - useful for broadcasting group statistics back to the original rows. -

-
-
TypeScript
-
// Subtract group mean (demeaning)
-const demeaned = df.groupby("dept").transform((vals, col) => {
-  if (col === "dept") return vals;
-  const nums = vals.filter((v): v is number => typeof v === "number");
-  const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
-  return vals.map((v) => (typeof v === "number" ? v - mean : v));
-});
-
-
Output (same shape, values demeaned)
- - - - - - - - - -
(index)deptsalesbonus
0A-5-0.5
1A50.5
2B-5-0.5
3B50.5
4C00
+ +
+

4 · std()

+

Sample standard deviation per group — numeric columns only (like pandas). Groups with fewer than 2 values return NaN.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  group:  ["A", "A", "A", "B", "B", "C"],
+  value:  [10, 20, 30, 100, 200, 42],
+});
+
+const result = df.groupby("group").std();
+console.log(result.toString());
+// Group C has only 1 row → std is NaN
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

5 · apply()

-

Run arbitrary logic on each sub-DataFrame and concatenate the results:

-
-
TypeScript
-
// Keep only the top-sales row from each dept
-const topRows = df.groupby("dept").apply((sub) =>
-  sub.sortValues("sales", false).head(1),
-);
-
-
Output
- - - - - - - -
(index)deptsalesbonus
1A202
3B404
4C505
+ +
+

5 · first() / last()

+

Return the first or last non-null value per group for each column.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "A", "B", "B"],
+  sales: [null, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  null],
+});
+
+console.log("=== first() ===");
+console.log(df.groupby("dept").first().toString());
+
+console.log("\n=== last() ===");
+console.log(df.groupby("dept").last().toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

6 · filter()

-

Keep only the rows belonging to groups that pass a predicate:

-
-
TypeScript
-
// Keep only groups with more than 1 row
-const big = df.groupby("dept").filter((sub) => sub.shape[0] > 1);
-
-
Output (C dropped — only 1 row)
- - - - - - - - -
(index)deptsalesbonus
0A101
1A202
2B303
3B404
+ +
+

6 · size(), ngroups, groupKeys

+

Inspect the structure of the groups. size() returns a Series with the count of rows per group (including nulls).

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+});
+
+const gb = df.groupby("dept");
+
+console.log("ngroups:", gb.ngroups);
+console.log("groupKeys:", gb.groupKeys);
+
+console.log("\nsize():");
+console.log(gb.size().toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

7 · agg() with named specs

+

Apply different aggregation functions to different columns using an object spec, or pass a custom function.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  5],
+});
+
+// Per-column named specs
+console.log("=== per-column specs ===");
+const result = df.groupby("dept").agg({
+  sales: "sum",
+  bonus: "mean",
+});
+console.log(result.toString());
+
+// Custom function: range = max − min
+console.log("\n=== custom agg (range) ===");
+const range = df.groupby("dept").agg((vals) => {
+  const nums = vals.filter((v) => typeof v === "number");
+  if (nums.length === 0) return 0;
+  return Math.max(...nums) - Math.min(...nums);
+});
+console.log(range.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

8 · transform()

+

+ Unlike agg(), transform() returns a same-shape DataFrame. + Useful for broadcasting group statistics back to the original rows. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  5],
+});
+
+// Subtract group mean (demeaning)
+const demeaned = df.groupby("dept").transform((vals, col) => {
+  if (col === "dept") return vals;
+  const nums = vals.filter((v) => typeof v === "number");
+  const mean = nums.reduce((a, b) => a + b, 0) / nums.length;
+  return vals.map((v) => (typeof v === "number" ? v - mean : v));
+});
+console.log(demeaned.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

7 · size() and ngroups

-

Inspect the structure of the groups:

-
-
TypeScript
-
const gb = df.groupby("dept");
-
-gb.ngroups;        // 3
-gb.groupKeys();   // ["A", "B", "C"]
-gb.size();        // Series { A: 2, B: 2, C: 1 }
-
-
gb.size()
- - - - - - - -
(index)size
A2
B2
C1
+ +
+

9 · apply()

+

Run arbitrary logic on each sub-DataFrame and concatenate the results vertically.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  5],
+});
+
+// Keep only the top-sales row from each dept
+const topRows = df.groupby("dept").apply((sub) =>
+  sub.sortValues("sales", false).head(1),
+);
+console.log(topRows.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

8 · Multi-key groupby

-

Group by an array of column names:

-
-
TypeScript
-
const df2 = DataFrame.fromColumns({
-  dept:   ["A", "A", "A", "B"],
-  region: ["E", "E", "W", "E"],
-  sales:  [10, 20, 30, 40],
+  
+  
+

10 · filter()

+

Keep only the rows belonging to groups that pass a predicate.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = DataFrame.fromColumns({
+  dept:  ["A", "A", "B", "B", "C"],
+  sales: [10, 20, 30, 40, 50],
+  bonus: [1,  2,  3,  4,  5],
 });
 
-df2.groupby(["dept", "region"]).sum();
-
-
3 composite groups
- - - - - - - -
(index)sales
A__SEP__E30
A__SEP__W30
B__SEP__E40
+// Keep only groups with more than 1 row +const big = df.groupby("dept").filter((sub) => sub.shape[0] > 1); +console.log("Groups with > 1 row (C dropped):"); +console.log(big.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

9 · SeriesGroupBy

-

Series also supports groupby(), accepting an array of key values:

-
-
TypeScript
-
import { Series } from "tsb";
-
-const s = new Series({ data: [1, 2, 3, 4] });
-s.groupby(["A", "A", "B", "B"]).sum();
-// Series { A: 3, B: 7 }
-
-
Output
- - - - - - -
(index)value
A3
B7
+ +
+

🧪 Scratch Pad

+

Write your own GroupBy code below. All exports from tsb are available: + DataFrame, Series, Index, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+
import { DataFrame, Series } from "tsb";
+
+// Try it! Build a DataFrame and explore the GroupBy API.
+const sales = DataFrame.fromColumns({
+  region:  ["East", "East", "West", "West", "East"],
+  quarter: [1, 2, 1, 2, 1],
+  revenue: [100, 150, 200, 250, 120],
+});
+
+console.log("Revenue by region:");
+console.log(sales.groupby("region").sum().toString());
+
+console.log("\nAverage revenue by region:");
+console.log(sales.groupby("region").mean().toString());
+
+console.log("\nGroup sizes:");
+console.log(sales.groupby("region").size().toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

API Reference

- - - - - - - - - - - - - - - - - - - - - - - - -
MethodDescription
groupby(by)Group by column name(s) → DataFrameGroupBy
.sum()Sum of each group (numeric columns)
.mean()Mean of each group
.min()Minimum per group
.max()Maximum per group
.count()Count non-null values per group
.std()Sample standard deviation per group
.first()First non-null value per group
.last()Last non-null value per group
.size()Number of rows per group (Series)
.agg(spec)Apply named agg / custom fn / per-column specs
.transform(fn)Same-shape result; broadcast group stats back
.apply(fn)Arbitrary per-group function; results concatenated
.filter(pred)Keep rows from groups that pass predicate
.getGroup(key)Extract sub-DataFrame for a single key
.ngroupsNumber of groups
.groupKeysArray of group key labels
.groupsMap from key → row labels
- - - -
- tsb — a TypeScript port of pandas, built from first principles. -
+ + + diff --git a/playground/index.html b/playground/index.html index 0575bb65..73ab3f0e 100644 --- a/playground/index.html +++ b/playground/index.html @@ -136,7 +136,7 @@

📐 Project Foundation

📊 Series

-

1-D labeled array — the core building block of tsb data structures.

+

1-D labeled array — Interactive Playground. The core building block of tsb data structures.

✅ Complete
@@ -151,7 +151,7 @@

🏷️ Index

🔢 Dtypes

-

Rich dtype system: int/float/bool/string/datetime/category.

+

Rich dtype system — Interactive Playground. int/float/bool/string/datetime/category.

✅ Complete
@@ -249,6 +249,21 @@

📈 cumulative operations

Compute running totals, products, maxima, and minima — interactive tutorial. cumsum(), cumprod(), cummax(), cummin() for Series and DataFrame with skipna support and axis=0/1.

✅ Complete
+
+

✂️ element-wise ops

+

Element-wise transformations — interactive tutorial. clip(), seriesAbs(), seriesRound() for Series and DataFrame with min/max bounds, decimal precision, and axis support.

+
✅ Complete
+
+
+

🔢 value_counts

+

Count unique values — interactive tutorial. valueCounts() for Series and dataFrameValueCounts() for DataFrame with normalize, sort, ascending, and dropna options.

+
✅ Complete
+
+
+

🗂️ MultiIndex

+

Hierarchical indexing — interactive tutorial. MultiIndex for multi-level row and column labels with fromArrays, fromTuples, fromProduct, level access, and swapLevels.

+
✅ Complete
+
diff --git a/playground/merge.html b/playground/merge.html index adf933db..f99707a3 100644 --- a/playground/merge.html +++ b/playground/merge.html @@ -3,7 +3,7 @@ - tsb — merge Tutorial + tsb — Merge Playground -
-

tsb

- ← back to index - merge — SQL-style DataFrame joins -
- -
-

Overview

-

- merge(left, right, options) is the tsb equivalent of pandas.merge / - DataFrame.merge. It performs SQL-style joins between two DataFrames, matching rows - by shared key column values. -

-

- Four join types are supported: "inner" (default), "left", - "right", and "outer". Key columns can be specified via - on, left_on/right_on, or using the row index - with left_index/right_index. -

+ +
+
+
Initializing playground…
+
- -

Inner join default

+ ← Back to roadmap +

🔗 Merge — Interactive Playground

- Returns only the rows where the key column value exists in both DataFrames. - This is equivalent to a SQL INNER JOIN. + merge(left, right, options) performs SQL-style joins between + two DataFrames — mirroring pandas.merge.
+ Four join types are supported: "inner" (default), + "left", "right", and "outer".
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser.

-
-
inner join — only matching rows are kept
-
import { DataFrame, merge } from "tsb";
+  
+  
+

1 · Basic inner merge

+

Returns only rows where the key exists in both DataFrames (SQL INNER JOIN). This is the default join type.

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, merge } from "tsb";
 
-const orders = DataFrame.fromColumns({
-  orderId:    [1, 2, 3, 4],
-  customerId: [10, 20, 10, 30],
-  amount:     [100, 200, 150, 80],
+const orders = DataFrame.fromColumns({
+  orderId:    [1, 2, 3, 4],
+  customerId: [10, 20, 10, 30],
+  amount:     [100, 200, 150, 80],
 });
-const customers = DataFrame.fromColumns({
-  customerId: [10, 20, 40],
-  name:       ["Alice", "Bob", "Dave"],
+const customers = DataFrame.fromColumns({
+  customerId: [10, 20, 40],
+  name:       ["Alice", "Bob", "Dave"],
 });
 
-// Default how="inner" — customers 30 and 40 have no match → excluded
-const result = merge(orders, customers, { on: "customerId" });
-result.toRecords();
-
-
Output (3 rows — customerId 10 matches twice, 20 once)
- - - - - -
customerIdorderIdamountname
101100Alice
103150Alice
202200Bob
+// Default how="inner" — customers 30 and 40 have no match → excluded +const result = merge(orders, customers, { on: "customerId" }); +console.log(result.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

Left join how="left"

-

- Keeps all rows from the left DataFrame. Right-side columns are filled with - null where no match is found. -

+ +
+

2 · Left, Right, and Outer joins

+

+ "left" keeps all left rows, "right" keeps all right rows, + and "outer" keeps all rows from both sides. Missing values are filled + with null. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, merge } from "tsb";
 
-  
-
left join — all orders retained, missing customer info → null
-
const result = merge(orders, customers, {
-  on:  "customerId",
-  how: "left",
-});
-result.toRecords();
-
-
Output (4 rows — order with customerId=30 gets null name)
- - - - - - -
customerIdorderIdamountname
101100Alice
103150Alice
202200Bob
30480null
-
-
- - -

Right join how="right"

-

- Keeps all rows from the right DataFrame. Left-side columns are filled with - null where no match is found. -

- -
-
right join — all customers retained, Dave (id=40) gets null order info
-
const result = merge(orders, customers, {
-  on:  "customerId",
-  how: "right",
-});
-result.toRecords();
-
-
Output (4 rows — customer 10 appears twice due to 2 orders)
- - - - - - -
customerIdorderIdamountname
101100Alice
103150Alice
202200Bob
40nullnullDave
-
-
+const left = DataFrame.fromColumns({ k: [1, 2], v: [10, 20] }); +const right = DataFrame.fromColumns({ k: [2, 3], w: [200, 300] }); - -

Outer join how="outer"

-

- Keeps all rows from both DataFrames. Fills null wherever - there is no matching row on the other side. -

+console.log("=== LEFT JOIN ==="); +console.log(merge(left, right, { on: "k", how: "left" }).toString()); -
-
outer join — full union of both sides
-
const left  = DataFrame.fromColumns({ k: [1, 2], v: [10, 20] });
-const right = DataFrame.fromColumns({ k: [2, 3], w: [200, 300] });
+console.log("\n=== RIGHT JOIN ===");
+console.log(merge(left, right, { on: "k", how: "right" }).toString());
 
-const result = merge(left, right, { on: "k", how: "outer" });
-result.toRecords();
-
-
Output (3 rows)
- - - - - -
kvw
110null
220200
3null300
+console.log("\n=== OUTER JOIN ==="); +console.log(merge(left, right, { on: "k", how: "outer" }).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

Joining on different column names

-

- When the key columns have different names in each DataFrame, use - left_on and right_on instead of on. - Both columns appear in the result. -

+ +
+

3 · Merge on different column names

+

+ When key columns have different names in each DataFrame, use + left_on and right_on instead of on. + Both key columns appear in the result. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, merge } from "tsb";
 
-  
-
left_on / right_on — employee id in left, generic id in right
-
const employees = DataFrame.fromColumns({
-  empId:  [1, 2, 3],
-  salary: [50000, 60000, 70000],
+const employees = DataFrame.fromColumns({
+  empId:  [1, 2, 3],
+  salary: [50000, 60000, 70000],
 });
-const departments = DataFrame.fromColumns({
-  id:   [2, 3, 4],
-  dept: ["Eng", "HR", "Fin"],
+const departments = DataFrame.fromColumns({
+  id:   [2, 3, 4],
+  dept: ["Eng", "HR", "Fin"],
 });
 
-const result = merge(employees, departments, {
-  left_on:  "empId",
-  right_on: "id",
+// empId in left matches id in right
+const result = merge(employees, departments, {
+  left_on:  "empId",
+  right_on: "id",
 });
-result.toRecords();
-
-
Output (2 rows — empId 2 and 3 match)
- - - - -
empIdsalaryiddept
2600002Eng
3700003HR
+console.log(result.toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

Suffix handling for overlapping columns

-

- When both DataFrames have a non-key column with the same name, tsb appends suffixes - (_x and _y by default) to disambiguate them. Override with - the suffixes option. -

+ +
+

4 · Custom suffixes for overlapping columns

+

+ When both DataFrames share a non-key column name, tsb appends + _x / _y by default. Override with the + suffixes option. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, merge } from "tsb";
 
-  
-
suffixes — both DataFrames have a "score" column
-
const left  = DataFrame.fromColumns({ id: [1, 2], score: [80, 90] });
-const right = DataFrame.fromColumns({ id: [1, 2], score: [75, 95] });
+const left  = DataFrame.fromColumns({ id: [1, 2], score: [80, 90] });
+const right = DataFrame.fromColumns({ id: [1, 2], score: [75, 95] });
 
-const result = merge(left, right, {
-  on:       "id",
-  suffixes: ["_pre", "_post"],
-});
-result.toRecords();
-
-
Output
- - - - -
idscore_prescore_post
18075
29095
+// Default suffixes: _x and _y +console.log("=== Default suffixes ==="); +console.log(merge(left, right, { on: "id" }).toString()); + +// Custom suffixes +console.log("\n=== Custom suffixes (_pre, _post) ==="); +console.log(merge(left, right, { + on: "id", + suffixes: ["_pre", "_post"], +}).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

Multiple key columns

-

- Pass an array to on to join on multiple columns simultaneously. -

+ +
+

🧪 Scratch Pad

+

Write your own merge code below. All exports from tsb are available: + DataFrame, Series, merge, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+
import { DataFrame, merge } from "tsb";
 
-  
-
composite key — year + month
-
const actuals = DataFrame.fromColumns({
-  year:  [2024, 2024, 2025],
-  month: [1,    2,    1],
-  sales: [1200, 980, 1400],
-});
-const targets = DataFrame.fromColumns({
-  year:   [2024, 2025],
-  month:  [1,    1],
-  target: [1000, 1300],
+// Try it! Build two DataFrames and explore the merge API.
+const products = DataFrame.fromColumns({
+  productId: [1, 2, 3],
+  name:      ["Widget", "Gadget", "Gizmo"],
+  price:     [9.99, 24.99, 14.99],
 });
 
-const result = merge(actuals, targets, { on: ["year", "month"] });
-result.toRecords();
-
-
Output (2 rows — Feb 2024 has no target)
- - - - -
yearmonthsalestarget
2024112001000
2025114001300
-
-
- - -

sort option

-

- Pass sort: true to sort the result by the first join-key column after merging. -

+const sales = DataFrame.fromColumns({ + productId: [1, 1, 2, 3, 3], + qty: [10, 5, 8, 3, 7], + region: ["East", "West", "East", "East", "West"], +}); -
-
sort=true — result sorted by key column
-
const left  = DataFrame.fromColumns({ k: [3, 1, 2], v: [30, 10, 20] });
-const right = DataFrame.fromColumns({ k: [1, 2, 3], w: [100, 200, 300] });
+console.log("Products joined with sales (inner):");
+console.log(merge(products, sales, { on: "productId" }).toString());
 
-const result = merge(left, right, { on: "k", sort: true });
-[...result.col("k").values]; // [1, 2, 3]
-
-
k column values
-
[1, 2, 3]
+console.log("\nAll products, even with no sales (left join):"); +console.log(merge(products, sales, { + on: "productId", + how: "left", +}).toString());
+
Click ▶ Run to execute
+
Ctrl+Enter to run
- -

API summary

-
-
MergeOptions interface
-
interface MergeOptions {
-  how?:         "inner" | "outer" | "left" | "right"; // default: "inner"
-  on?:          string | string[];         // shared column(s)
-  left_on?:     string | string[];         // key column(s) in left
-  right_on?:    string | string[];         // key column(s) in right
-  left_index?:  boolean;                   // use left row-index as key
-  right_index?: boolean;                   // use right row-index as key
-  suffixes?:    [string, string];          // default: ["_x", "_y"]
-  sort?:        boolean;                   // sort result by key column
-}
-
- - - + + + diff --git a/playground/playground-runtime.js b/playground/playground-runtime.js index c0f25b63..6cba3e49 100644 --- a/playground/playground-runtime.js +++ b/playground/playground-runtime.js @@ -5,34 +5,55 @@ * * Architecture: * 1. Loads the tsb browser bundle (built by CI into playground/dist/) - * 2. Loads the TypeScript compiler from CDN for in-browser transpilation + * 2. Loads the TypeScript compiler (local bundle first, CDN fallback) * 3. Converts playground blocks into editable editors with Run/Reset buttons * 4. Transforms imports, transpiles TS → JS, and executes with output capture * * No WASM needed — the TypeScript compiler runs natively in JavaScript. */ -// ── Load TypeScript compiler from CDN ────────────────────────────── +// ── Load TypeScript compiler (local bundle → CDN fallback) ───────── -function loadTypeScript() { +function loadScriptWithTimeout(src, timeoutMs) { return new Promise(function (resolve, reject) { - if (window.ts) { - resolve(window.ts); - return; - } var script = document.createElement("script"); - script.src = - "https://cdn.jsdelivr.net/npm/typescript@5/lib/typescript.js"; + var timer = setTimeout(function () { + reject(new Error("Timeout loading " + src)); + }, timeoutMs); + script.src = src; script.onload = function () { - resolve(window.ts); + clearTimeout(timer); + resolve(); }; script.onerror = function () { - reject(new Error("Failed to load TypeScript compiler from CDN")); + clearTimeout(timer); + reject(new Error("Failed to load " + src)); }; document.head.appendChild(script); }); } +function loadTypeScript() { + if (window.ts) return Promise.resolve(window.ts); + + // Try local bundle first (built by CI), then fall back to CDN + return loadScriptWithTimeout("./dist/typescript.js", 15000) + .catch(function () { + return loadScriptWithTimeout( + "https://cdn.jsdelivr.net/npm/typescript@5/lib/typescript.js", + 30000, + ); + }) + .then(function () { + if (!window.ts) { + throw new Error( + "TypeScript compiler loaded but window.ts is not available", + ); + } + return window.ts; + }); +} + // ── Load tsb browser bundle ──────────────────────────────────────── function loadTsb() { @@ -130,6 +151,24 @@ function executeCode(jsCode) { return outputs.join("\n"); } +// ── Editor abstraction (supports both +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Properties

+

Inspect the size, shape, dtype, and other metadata of a Series.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Element Access

+

Use at() / iat() for single elements, and loc() / iloc() for label-based or positional slicing.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Arithmetic

+

Element-wise operations with a scalar or another Series: add, sub, mul, div, mod, pow.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Comparison

+

Element-wise comparison returns a boolean Series: eq, ne, lt, le, gt, ge.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Filtering & Boolean Masking

+

Use filter() with a boolean array or boolean Series to select elements.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Missing Values

+

Detect, drop, and fill null / NaN values.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Statistics

+

Aggregation and descriptive statistics: sum, mean, std, median, unique, valueCounts, and more.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

Sorting

+

Sort by values or by index labels, with control over direction and NA placement.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + +
+

🧪 Try It Yourself

+

Write your own tsb code below. All exports from tsb are available: + Series, Index, RangeIndex, Dtype, and more.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + + + + + + diff --git a/playground/serve.ts b/playground/serve.ts new file mode 100644 index 00000000..b542d948 --- /dev/null +++ b/playground/serve.ts @@ -0,0 +1,87 @@ +/** + * Local development server for the tsb playground. + * + * Usage: bun run playground (or: bun run playground/serve.ts) + * + * Builds the tsb browser bundle and TypeScript compiler into playground/dist/, + * then serves the playground on http://localhost:3000. + */ + +import { existsSync, mkdirSync } from "node:fs"; +import { extname, join } from "node:path"; + +const PORT = 3000; +const PLAYGROUND_DIR = import.meta.dir; +const PROJECT_ROOT = join(PLAYGROUND_DIR, ".."); +const DIST_DIR = join(PLAYGROUND_DIR, "dist"); + +const MIME_TYPES: Record = { + ".html": "text/html", + ".js": "text/javascript", + ".css": "text/css", + ".json": "application/json", + ".png": "image/png", + ".svg": "image/svg+xml", +}; + +async function buildBundle(): Promise { + console.info("Building tsb browser bundle…"); + const result = await Bun.build({ + entrypoints: [join(PROJECT_ROOT, "src", "index.ts")], + outdir: DIST_DIR, + target: "browser", + minify: true, + }); + if (!result.success) { + for (const log of result.logs) { + console.error(log); + } + process.exit(1); + } + console.info(" → playground/dist/index.js"); +} + +async function copyTypeScript(): Promise { + const src = join(PROJECT_ROOT, "node_modules", "typescript", "lib", "typescript.js"); + const dest = join(DIST_DIR, "typescript.js"); + const srcFile = Bun.file(src); + if (!(await srcFile.exists())) { + console.warn("⚠ TypeScript compiler not found. Run `npm install` first."); + return; + } + await Bun.write(dest, srcFile); + console.info(" → playground/dist/typescript.js"); +} + +async function main(): Promise { + if (!existsSync(DIST_DIR)) { + mkdirSync(DIST_DIR, { recursive: true }); + } + + await buildBundle(); + await copyTypeScript(); + + console.info(`\nPlayground ready at http://localhost:${PORT}\n`); + + Bun.serve({ + port: PORT, + fetch(req: Request): Response { + const url = new URL(req.url); + const pathname = url.pathname === "/" ? "/index.html" : url.pathname; + const filePath = join(PLAYGROUND_DIR, pathname); + + const file = Bun.file(filePath); + const ext = extname(filePath); + const contentType = MIME_TYPES[ext] ?? "application/octet-stream"; + + return new Response(file, { + headers: { "Content-Type": contentType }, + }); + }, + error(): Response { + return new Response("Not found", { status: 404 }); + }, + }); +} + +main(); diff --git a/src/core/cat_accessor.ts b/src/core/cat_accessor.ts index 050b9bfd..df7d34a6 100644 --- a/src/core/cat_accessor.ts +++ b/src/core/cat_accessor.ts @@ -76,6 +76,35 @@ function buildCodeMap(categories: readonly Scalar[]): Map { return m; } +/** + * Lightweight CatSeriesLike that avoids circular-importing the real Series. + * Used only by CategoricalAccessor.valueCounts to emit a result whose length + * differs from the original series. + */ +class PlainSeries implements CatSeriesLike { + readonly values: readonly Scalar[]; + readonly index: Index