Docs & Interactive Playground: https://df-script.js.org
AI Agent Docs: https://df-script.js.org/llms-full.txt
GitHub Repository: https://github.com/trentamorris/df-script
Install: npm install df-script
License: MIT
Overview
Heavily inspired by modern columnar engines like Polars and Pandas, df-script packs 300+ declarative operations into a 43kB, zero-dependency DataFrame engine for modern JavaScript and TypeScript environments. It lets you easily execute complex multi-column groupings, relational joins, windowed rolling statistics, string parsing, temporal conversions, and much more with a fluent, expressive API.
Why df-script?
For most web applications, performing complex data transformations requires maintaining a dedicated backend service. Suddenly, an otherwise straightforward application introduces deployment pipelines, network latency, serialization bottlenecks, asynchronous state management, and ongoing cloud hosting bills, all just to run operations modern client CPUs can perform in milliseconds.
To avoid this, many developers implement hand-written functions or native chains of .map(), .filter(), and .reduce() to transform data directly in the browser. These chained operations repeatedly traverse the dataset and flood memory with intermediate objects, degrading performance and freezing the UI. Worse, what starts as a simple utility quickly devolves into hundreds of lines of fragile boilerplate once real-world edge cases surface.
df-script changes this equation entirely. It introduces a 43kB zero-dependency, column-oriented DataFrame engine constructed specifically to enable complex chained data transformations directly in the browser with zero network latency and no server maintenance. By avoiding high-level array iterators like .map() and .reduce() in favor of contiguous columnar loops, df-script drastically cuts intermediate object allocations to reduce garbage collection pressure. Every function is backed by a dedicated 1:1 test suite covering hundreds of extreme edge cases, so you never have to hand-roll fragile data logic again.
Built-In CSV & NDJSON Ingestion + Multi-Metric GroupBys
You don't need external parsers to ingest raw text feeds. df-script parses CSV, JSON, and NDJSON directly into columnar storage and runs declarative aggregations with full TypeScript inference:
import { $df } from "df-script"; const rawCsv = `region,category,revenue,quantity,timestamp East,Hardware,450,3,2026-02-01 East,Hardware,550,5,2026-02-10 West,Software,1200,1,2026-01-20`; const salesSummary = $df .readCsv(rawCsv) .groupBy(["region", "category"]) .agg([ $df.col("revenue").sum().alias("total_revenue"), $df.col("revenue").mean().round(2).alias("avg_revenue"), $df.col("quantity").sum().alias("units_sold"), $df.col("timestamp").str.toDatetime().dt.year().alias("year"), ]) .sort("total_revenue", { descending: true }); console.log(salesSummary.toDicts()); /* +--------+----------+---------------+-------------+------------+------+ | region | category | total_revenue | avg_revenue | units_sold | year | +--------+----------+---------------+-------------+------------+------+ | West | Software | 1200 | 1200 | 1 | 2026 | | East | Hardware | 1000 | 500 | 8 | 2026 | +--------+----------+---------------+-------------+------------+------+ */ Partitioned Window Functions (.over()) & Rolling Metrics
Compute group-partitioned metrics, cumulative sums, rolling statistics, and exponentially weighted moving averages without manual grouping and merging:
const transactions = $df.data([ { region: "East", revenue: 450 }, { region: "East", revenue: 550 }, { region: "West", revenue: 1200 }, ]); /* transactions: +--------+---------+ | region | revenue | +--------+---------+ | East | 450 | | East | 550 | | West | 1200 | +--------+---------+ */ const ranked = transactions.withColumns([ $df.col("revenue").sum().over("region").alias("region_total"), $df.col("revenue").rowNumber().over("region").alias("region_rank"), $df.col("revenue").rollingMean(3).alias("rolling_3_avg"), ]); console.log(ranked.toDicts()); /* Output: +--------+---------+--------------+-------------+---------------+ | region | revenue | region_total | region_rank | rolling_3_avg | +--------+---------+--------------+-------------+---------------+ | East | 450 | 1000 | 1 | 450 | | East | 550 | 1000 | 2 | 500 | | West | 1200 | 1200 | 1 | 733.33 | +--------+---------+--------------+-------------+---------------+ */ First-Class Nested Array Expressions (.arr.*)
const df = $df.data({ user: ["Alice", "Bob"], scores: [[72, 88, 95], [60, 65]], }); /* df: +-------+--------------+ | user | scores | +-------+--------------+ | Alice | [72, 88, 95] | | Bob | [60, 65] | +-------+--------------+ */ const analyzed = df.withColumns([ $df.col("scores").arr.filter($df.element().gt(80)).alias("high_scores"), $df.col("scores").arr.mean().round(1).alias("avg_score"), ]); console.log(analyzed.toDicts()); /* Output: +-------+--------------+-------------+-----------+ | user | scores | high_scores | avg_score | +-------+--------------+-------------+-----------+ | Alice | [72, 88, 95] | [88, 95] | 85 | | Bob | [60, 65] | [] | 62.5 | +-------+--------------+-------------+-----------+ */ Relational Joins
Join tables (left, inner, outer, semi, anti, and asof) with automatic suffixing:
const orders = $df.data([ { orderId: 101, customerId: 1, amount: 250 }, { orderId: 102, customerId: 2, amount: 1400 }, ]); /* orders: +---------+------------+--------+ | orderId | customerId | amount | +---------+------------+--------+ | 101 | 1 | 250 | | 102 | 2 | 1400 | +---------+------------+--------+ */ const customers = $df.data([ { id: 1, firstName: "Alice", lastName: "Smith" }, { id: 2, firstName: "Bob", lastName: "Jones" }, ]); /* customers: +----+-----------+----------+ | id | firstName | lastName | +----+-----------+----------+ | 1 | Alice | Smith | | 2 | Bob | Jones | +----+-----------+----------+ */ const enrichedOrders = orders .join(customers, { leftOn: "customerId", rightOn: "id", how: "left", }) .withColumns([ $df.col("firstName").str.concat(" ").str.concat($df.col("lastName")).alias("fullName"), $df.when($df.col("amount").gt(1000)).then("VIP").otherwise("Standard").alias("tier"), ]); console.log(enrichedOrders.toDicts()); /* Output: +---------+------------+--------+----+-----------+----------+-------------+----------+ | orderId | customerId | amount | id | firstName | lastName | fullName | tier | +---------+------------+--------+----+-----------+----------+-------------+----------+ | 101 | 1 | 250 | 1 | Alice | Smith | Alice Smith | Standard | | 102 | 2 | 1400 | 2 | Bob | Jones | Bob Jones | VIP | +---------+------------+--------+----+-----------+----------+-------------+----------+ */ df-script is completely free and open-source under the MIT License.
I'd truly appreciate any feedback, edge-case ideas, or thoughts on what you'd like to see added next!

