From a6024cc039c0b84cccd5df5d0f7613af9bf181bc Mon Sep 17 00:00:00 2001 From: Aurelien LAJOIE Date: Sat, 23 May 2026 18:53:26 +0200 Subject: [PATCH 1/4] Rewrite in TypeScript with tests, validation, and improved rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace CoffeeScript source with TypeScript (src/babar.ts, buckets.ts, draw.ts, types.ts, validate.ts) - Replace `colors` with `chalk` (no String.prototype mutation) - Add input validation with clear error messages (empty array, non-finite values, bad options) - Fix crash bugs: diffX=0, diff=0, numBkts=1 division by zero - Upgrade partial bar rendering from 2 levels (▄) to 8 levels (▁▂▃▄▅▆▇█) - Restore horizontal grid lines (_) above bars, matching original behavior - Add 48 unit and integration tests with Vitest - Add advanced.js example covering 13 corner cases - Bump version to 0.3.0 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- examples/advanced.js | 183 ++++ examples/simple.js | 2 +- package-lock.json | 2056 +++++++++++++++++++++++++++++++++++++++++ package.json | 23 +- src/babar.ts | 107 +++ src/buckets.ts | 89 ++ src/draw.ts | 110 +++ src/types.ts | 28 + src/validate.ts | 57 ++ test/babar.test.ts | 90 ++ test/buckets.test.ts | 80 ++ test/draw.test.ts | 67 ++ test/validate.test.ts | 61 ++ tsconfig.json | 17 + 14 files changed, 2960 insertions(+), 10 deletions(-) create mode 100644 examples/advanced.js create mode 100644 package-lock.json create mode 100644 src/babar.ts create mode 100644 src/buckets.ts create mode 100644 src/draw.ts create mode 100644 src/types.ts create mode 100644 src/validate.ts create mode 100644 test/babar.test.ts create mode 100644 test/buckets.test.ts create mode 100644 test/draw.test.ts create mode 100644 test/validate.test.ts create mode 100644 tsconfig.json diff --git a/examples/advanced.js b/examples/advanced.js new file mode 100644 index 0000000..b5c922c --- /dev/null +++ b/examples/advanced.js @@ -0,0 +1,183 @@ +var babar = require('../lib/babar'); + +function section(title) { + var w = 72; + var inner = title.length + 2; + var pad = Math.max(0, Math.floor((w - inner) / 2)); + var right = Math.max(0, w - inner - pad); + var line = '─'.repeat(pad) + ' ' + title + ' ' + '─'.repeat(right); + console.log('\n' + line); +} + +// ───────────────────────────────────────────────────────────────────────────── +section('1. just above the floor — bar cap sitting exactly at the grid bottom'); +// All values cluster just above minY so the bars are 1–2 rows tall max. +// Tests that partial blocks render correctly at very low heights. +var barelyAbove = []; +for (var i = 0; i < 40; i++) { + barelyAbove.push([i, 100 + Math.sin(i * 0.4) * 0.9]); +} +console.log(babar(barelyAbove, { + caption: 'sin wave compressed near floor (minY=99, maxY=103)', + color: 'magenta', + grid: 'grey', + width: 72, + height: 12, + minY: 99, + maxY: 103, + yFractions: 2, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('2. just below the ceiling — bar cap touching the top row'); +// Values crowd the top, testing that ▇/█ at the topmost row don't overflow. +var nearCeiling = []; +for (var i = 0; i < 40; i++) { + nearCeiling.push([i, 99 + Math.cos(i * 0.3) * 0.95]); +} +console.log(babar(nearCeiling, { + caption: 'cos wave compressed near ceiling (minY=97, maxY=101)', + color: 'cyan', + grid: 'grey', + width: 72, + height: 12, + minY: 97, + maxY: 101, + yFractions: 2, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('3. single point — minimum viable dataset'); +console.log(babar([[0, 42]], { + caption: 'single point', + color: 'yellow', + grid: 'grey', + width: 30, + height: 6, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('4. all identical Y — flat chart (diff = 0)'); +console.log(babar([[0,7],[1,7],[2,7],[3,7],[4,7],[5,7]], { + caption: 'all values = 7', + color: 'green', + grid: 'grey', + width: 40, + height: 8, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('5. single unique X with many Y values — diffX = 0'); +console.log(babar([[5,1],[5,3],[5,7],[5,2],[5,9]], { + caption: 'all points at x=5 (avg=4.4)', + color: 'blue', + grid: 'grey', + width: 30, + height: 8, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('6. negative Y range — bars below zero'); +var negPts = []; +for (var i = 0; i < 30; i++) { + negPts.push([i, Math.sin(i * 0.5) * 50]); +} +console.log(babar(negPts, { + caption: 'sin wave from -50 to +50', + color: 'red', + grid: 'grey', + width: 72, + height: 14, + yFractions: 1, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('7. very sparse data — 3 points over wide x range with many empty buckets'); +console.log(babar([[0, 10], [50, 90], [100, 40]], { + caption: '3 points → empty buckets interpolate from neighbours', + color: 'yellow', + grid: 'grey', + width: 72, + height: 10, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('8. dense data — many points per bucket (averaging)'); +var densePts = []; +for (var i = 0; i < 2000; i++) { + var x = Math.floor(i / 20); // 100 unique x values + var noise = (Math.random() - 0.5) * 30; + densePts.push([x, Math.sin(x * 0.2) * 40 + 50 + noise]); +} +console.log(babar(densePts, { + caption: '2000 points → 100 buckets averaged (sin + noise)', + color: 'cyan', + grid: 'grey', + width: 72, + height: 14, + yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('9. tiny terminal — minimum size'); +console.log(babar([[0,1],[1,3],[2,2]], { + caption: 'tiny', + color: 'green', + grid: 'grey', + width: 16, + height: 5, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('10. exponential growth — tests label width with large numbers'); +var expPts = []; +for (var i = 0; i < 20; i++) { + expPts.push([i, Math.pow(2, i)]); +} +console.log(babar(expPts, { + caption: '2^x from 1 to 524288', + color: 'magenta', + grid: 'grey', + width: 72, + height: 14, + yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('11. clipped window — maxY cuts off spikes'); +var spiky = [[0,5],[1,5],[2,5],[3,999],[4,5],[5,5],[6,999],[7,5],[8,5]]; +console.log(babar(spiky, { + caption: 'spikes at x=3 and x=6 clipped by maxY=10', + color: 'red', + grid: 'grey', + width: 72, + height: 10, + maxY: 10, + yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('12. sub-row precision showcase — staircase through all 8 block levels'); +// Build a staircase so each bar cap falls exactly at k/8 fractions +var stairPts = []; +for (var i = 0; i < 9; i++) { + stairPts.push([i, i / 8]); // 0/8, 1/8, 2/8 … 8/8 +} +console.log(babar(stairPts, { + caption: 'staircase: each bar cap at k/8 — shows ▁▂▃▄▅▆▇█', + color: 'cyan', + grid: 'grey', + width: 72, + height: 10, + yFractions: 3, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('13. ascii fallback — same staircase without color'); +console.log(babar(stairPts, { + caption: 'staircase in ascii mode', + color: 'ascii', + width: 72, + height: 10, + yFractions: 3, +})); diff --git a/examples/simple.js b/examples/simple.js index 07761ab..75518ba 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -40,7 +40,7 @@ console.log(babar([ ], { width: 80, grid: 'blue', - height: 10, + height: 11, color: 'yellow', maxY: 100 })); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d56497e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2056 @@ +{ + "name": "babar", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "babar", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "rimraf": "^3.0.2", + "typescript": "^5.4.5", + "vitest": "^1.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 2be33a5..86c9902 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,20 @@ { "name": "babar", - "version": "0.2.3", + "version": "0.3.0", "description": "CLI bar charts", "main": "lib/babar.js", + "types": "lib/babar.d.ts", + "type": "commonjs", "scripts": { - "build:lib": "coffee -o lib -c src/babar.coffee", + "build": "tsc", "clean": "rimraf lib coverage", "preversion": "npm run clean", - "version": "npm run build:lib", + "version": "npm run build", "postversion": "git push && git push --tags && npm run clean", - "prepublish": "npm run clean && npm run build:lib", - "dev": "coffee --watch -o lib/ -c src/babar.coffee", - "test": "echo \"Error: no test specified\" && exit 1" + "prepublish": "npm run clean && npm run build", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "repository": { "type": "git", @@ -30,10 +33,12 @@ "url": "https://github.com/stephan83/babar/issues" }, "dependencies": { - "colors": "~1.4.0" + "chalk": "^4.1.2" }, "devDependencies": { - "coffeescript": "~1.6.3", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "typescript": "^5.4.5", + "vitest": "^1.6.0", + "@types/node": "^20.0.0" } } diff --git a/src/babar.ts b/src/babar.ts new file mode 100644 index 0000000..b16ab95 --- /dev/null +++ b/src/babar.ts @@ -0,0 +1,107 @@ +import chalk from 'chalk'; +import { validatePoints, validateOptions } from './validate.js'; +import { computeStats, bucketize } from './buckets.js'; +import { drawChart } from './draw.js'; +import type { Point, Options, Color } from './types.js'; + +export type { Point, Options, Color }; + +function clamp(min: number, max: number, val: number): number { + return Math.max(min, Math.min(max, val)); +} + +function repeat(n: number, ch: string): string { + if (n <= 0) return ''; + return ch.repeat(n); +} + +export default function babar(points: Point[], options: Options = {}): string { + validatePoints(points); + validateOptions(options); + + const color: Color = options.color ?? 'cyan'; + const grid: Color = options.grid ?? 'black'; + let width = options.width ?? 80; + let height = options.height ?? 15; + + const stats = computeStats(points); + const minX = options.minX ?? stats.minX; + const maxX = options.maxX ?? stats.maxX; + const minY = options.minY; + const maxY = options.maxY; + + const diffX = maxX - minX; + const diffY = (maxY ?? stats.maxY) - (minY ?? stats.minY); + + const caption = options.caption; + height -= 1 + (caption !== undefined ? 1 : 0); + + const yFractions = options.yFractions ?? + clamp(0, 8, Math.log(height / (diffY || 1) * 5) / Math.LN10); + + const effectiveMinY = minY ?? stats.minY; + const effectiveMaxY = maxY ?? stats.maxY; + + const lblYW = 1 + Math.max( + effectiveMinY.toFixed(yFractions).length, + effectiveMaxY.toFixed(yFractions).length, + ); + + width -= lblYW; + + const numBkts = Math.max(1, Math.min(stats.uniqueX, width)); + const bktW = Math.max(1, Math.floor(width / numBkts)); + + const xFractions = options.xFractions ?? + clamp(0, 8, Math.log(numBkts / (diffX || 1) * 5) / Math.LN10); + + const { bkt, min, max, diff } = bucketize( + points, numBkts, minX, diffX, minY, maxY, height, + ); + + // Build Y axis labels (bottom to top) + const lblY: string[] = []; + for (let v = height - 1; v >= 0; v--) { + lblY.unshift((min + diff * v / Math.max(1, height - 1)).toFixed(yFractions)); + } + + // Compute X label density + let lblXW = 0; + for (let u = 0; u < numBkts; u++) { + const lbl = (minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions); + lblXW = Math.max(lblXW, lbl.length); + } + + let lblXN = numBkts; + let lblXI = 1; + while ((lblXN + 1) * lblXW >= numBkts * bktW) { + lblXN = Math.floor(lblXN / 2); + lblXI *= 2; + } + + let out = ''; + + if (caption !== undefined) { + out += repeat(lblYW, ' '); + out += color === 'ascii' ? caption : chalk.bold(caption); + out += '\n'; + } + + out += drawChart(height, lblY, lblYW, bkt, bktW, color, grid) + '\n'; + out += repeat(lblYW, ' '); + + for (let x = 0; x < lblXN; x++) { + const u = x * lblXI; + const lbl = (minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions); + out += lbl; + out += repeat(bktW * lblXI - lbl.length, ' '); + } + + return out; +} + +// Allow `require('babar')` to work directly without `.default` +if (typeof module !== 'undefined') { + module.exports = babar; + module.exports.default = babar; +} diff --git a/src/buckets.ts b/src/buckets.ts new file mode 100644 index 0000000..4f18639 --- /dev/null +++ b/src/buckets.ts @@ -0,0 +1,89 @@ +import type { Point } from './types.js'; + +export interface PointStats { + minX: number; + maxX: number; + minY: number; + maxY: number; + uniqueX: number; +} + +export function computeStats(points: Point[]): PointStats { + const seenX = new Set(); + let minX = Infinity, maxX = -Infinity; + let minY = Infinity, maxY = -Infinity; + + for (const [x, y] of points) { + seenX.add(x); + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + + return { minX, maxX, minY, maxY, uniqueX: seenX.size }; +} + +export function createBuckets( + points: Point[], + numBkts: number, + minX: number, + diffX: number, +): Point[][] { + const bkt: Point[][] = Array.from({ length: numBkts }, () => []); + + for (const p of points) { + const u = diffX === 0 + ? 0 + : Math.min(numBkts - 1, Math.floor((p[0] - minX) / diffX * numBkts)); + bkt[u].push(p); + } + + return bkt; +} + +export function avgBuckets(bkt: Point[][]): number[] { + const result: number[] = []; + let prev = 0; + + for (const values of bkt) { + if (values.length > 0) { + prev = values.reduce((sum, p) => sum + p[1], 0) / values.length; + } + result.push(prev); + } + + return result; +} + +export function normalizeBuckets(bkt: number[], min: number, diff: number, h: number): number[] { + if (diff === 0) return bkt.map(() => h / 2); + return bkt.map(v => (v - min) / diff * h); +} + +export interface BucketizeResult { + bkt: number[]; + min: number; + max: number; + diff: number; +} + +export function bucketize( + points: Point[], + numBkts: number, + minX: number, + diffX: number, + minY: number | undefined, + maxY: number | undefined, + h: number, +): BucketizeResult { + const raw = avgBuckets(createBuckets(points, numBkts, minX, diffX)); + let min = Math.min(...raw); + let max = Math.max(...raw); + + if (maxY !== undefined) max = maxY; + if (minY !== undefined) min = minY; + + const diff = max - min; + return { bkt: normalizeBuckets(raw, min, diff, h - 1), min, max, diff }; +} diff --git a/src/draw.ts b/src/draw.ts new file mode 100644 index 0000000..bad48c6 --- /dev/null +++ b/src/draw.ts @@ -0,0 +1,110 @@ +import chalk from 'chalk'; +import type { Color } from './types.js'; + +// 8-level vertical block characters: index 0 = empty, 8 = full block +const BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] as const; +const BLOCKS_ASCII_FILL = 'X'; + +function repeat(n: number, ch: string): string { + if (n <= 0) return ''; + return ch.repeat(n); +} + +function colorize(str: string, color: Color): string { + if (color === 'ascii') return str; + const fn = (chalk as unknown as Record string>)[color]; + return fn ? fn(str) : str; +} + +/** + * Returns the block character for a fractional fill level [0..1]. + * 0 → empty, 1 → full █ + */ +function blockChar(fraction: number): string { + const idx = Math.round(fraction * 8); + return BLOCKS[Math.min(8, Math.max(0, idx))]; +} + +/** + * Render one column cell at row `r` for a bar of normalized height `v`. + * `v` is in [0..height-1] coordinate space (floats). + * + * Row 0 is the bottom, row (height-1) is the top. + * For each row: + * - row < floor(v) → full block (bar body) + * - row = floor(v) → partial block (bar cap, fractional part of v) + * - row > floor(v) → empty (above bar) + */ +function cellChar(r: number, v: number, ascii: boolean): string { + const full = Math.floor(v); + if (r < full) { + return ascii ? BLOCKS_ASCII_FILL : '█'; + } + if (r === full) { + const frac = v - full; + if (ascii) return frac >= 0.5 ? BLOCKS_ASCII_FILL : ' '; + return blockChar(frac); + } + return ' '; +} + +export function drawRowChart( + r: number, + bkt: number[], + bktW: number, + color: Color, + grid: Color, +): string { + const ascii = color === 'ascii'; + let out = ''; + + for (const v of bkt) { + const ch = cellChar(r, v, ascii); + + if (ch === ' ') { + // above bar: full cell is horizontal grid line + out += ascii ? repeat(bktW, ' ') : colorize(repeat(bktW, '_'), grid); + } else { + // bar body or cap: colored fill, right edge is a grid underscore + const bodyW = Math.max(1, bktW - 1); + const colored = ascii ? repeat(bodyW, ch) : colorize(repeat(bodyW, ch), color); + const edge = bktW > 1 ? (ascii ? ' ' : colorize('_', grid)) : ''; + out += colored + edge; + } + } + + return out; +} + +export function drawRowLabel(r: number, lblY: string[], lblYW: number): string { + const lbl = r === 0 || lblY[r] !== lblY[r - 1] ? lblY[r] : ''; + return repeat(lblYW - lbl.length - 1, ' ') + lbl; +} + +export function drawRow( + r: number, + lblY: string[], + lblYW: number, + bkt: number[], + bktW: number, + color: Color, + grid: Color, +): string { + return `${drawRowLabel(r, lblY, lblYW)} ${drawRowChart(r, bkt, bktW, color, grid)}`; +} + +export function drawChart( + h: number, + lblY: string[], + lblYW: number, + bkt: number[], + bktW: number, + color: Color, + grid: Color, +): string { + const rows: string[] = []; + for (let r = h - 1; r >= 0; r--) { + rows.push(drawRow(r, lblY, lblYW, bkt, bktW, color, grid)); + } + return rows.join('\n'); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3ad33db --- /dev/null +++ b/src/types.ts @@ -0,0 +1,28 @@ +export type Point = [number, number]; + +export type Color = + | 'ascii' + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'grey' + | 'gray'; + +export interface Options { + caption?: string; + color?: Color; + grid?: Color; + width?: number; + height?: number; + xFractions?: number; + yFractions?: number; + minX?: number; + maxX?: number; + minY?: number; + maxY?: number; +} diff --git a/src/validate.ts b/src/validate.ts new file mode 100644 index 0000000..9dade57 --- /dev/null +++ b/src/validate.ts @@ -0,0 +1,57 @@ +import type { Point, Options, Color } from './types.js'; + +const VALID_COLORS: Color[] = [ + 'ascii', 'black', 'red', 'green', 'yellow', + 'blue', 'magenta', 'cyan', 'white', 'grey', 'gray', +]; + +export function validatePoints(points: unknown): asserts points is Point[] { + if (!Array.isArray(points)) { + throw new TypeError('babar: points must be an array'); + } + if (points.length === 0) { + throw new RangeError('babar: points array must not be empty'); + } + for (let i = 0; i < points.length; i++) { + const p = points[i]; + if (!Array.isArray(p) || p.length < 2) { + throw new TypeError(`babar: points[${i}] must be a [x, y] array`); + } + if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) { + throw new RangeError(`babar: points[${i}] contains non-finite value`); + } + } +} + +export function validateOptions(opts: Options): void { + if (opts.color !== undefined && !VALID_COLORS.includes(opts.color)) { + throw new TypeError( + `babar: invalid color "${opts.color}". Valid values: ${VALID_COLORS.join(', ')}` + ); + } + if (opts.grid !== undefined && !VALID_COLORS.includes(opts.grid)) { + throw new TypeError( + `babar: invalid grid color "${opts.grid}". Valid values: ${VALID_COLORS.join(', ')}` + ); + } + if (opts.width !== undefined && opts.width < 3) { + throw new RangeError('babar: width must be at least 3'); + } + if (opts.height !== undefined && opts.height < 3) { + throw new RangeError('babar: height must be at least 3'); + } + if ( + opts.minX !== undefined && + opts.maxX !== undefined && + opts.minX >= opts.maxX + ) { + throw new RangeError('babar: minX must be less than maxX'); + } + if ( + opts.minY !== undefined && + opts.maxY !== undefined && + opts.minY >= opts.maxY + ) { + throw new RangeError('babar: minY must be less than maxY'); + } +} diff --git a/test/babar.test.ts b/test/babar.test.ts new file mode 100644 index 0000000..81b7f6d --- /dev/null +++ b/test/babar.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from 'vitest'; +import babar from '../src/babar.js'; +import type { Point } from '../src/types.js'; + +const pts: Point[] = [[0, 1], [1, 5], [2, 5], [3, 1], [4, 6]]; + +describe('babar (integration)', () => { + it('returns a non-empty string', () => { + const result = babar(pts); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + }); + + it('output has expected number of lines (height + x labels)', () => { + const height = 10; + const result = babar(pts, { height, color: 'ascii' }); + const lines = result.split('\n'); + // height - 1 (x label row) - 1 (caption adjustment) = 8 chart rows + 1 x-label trailing line + // Actual: height param = 10, internal = 10 - 1 = 9 chart rows + '\n' + x-label (no trailing \n) + expect(lines.length).toBeGreaterThanOrEqual(height - 2); + }); + + it('includes caption when provided', () => { + const result = babar(pts, { caption: 'My Chart', color: 'ascii' }); + expect(result).toContain('My Chart'); + }); + + it('ascii mode uses X characters for bars', () => { + const result = babar([[0, 5], [1, 5]], { color: 'ascii', width: 20, height: 5 }); + expect(result).toContain('X'); + }); + + it('handles single point', () => { + expect(() => babar([[0, 42]])).not.toThrow(); + }); + + it('handles all same Y values (diff=0)', () => { + expect(() => babar([[0, 5], [1, 5], [2, 5]])).not.toThrow(); + }); + + it('handles single unique X (diffX=0)', () => { + expect(() => babar([[3, 1], [3, 2], [3, 5]])).not.toThrow(); + }); + + it('respects minY/maxY clipping', () => { + const result = babar(pts, { minY: 0, maxY: 10, color: 'ascii' }); + expect(result).toContain('0'); + expect(result).toContain('10'); + }); + + it('renders negative Y values', () => { + expect(() => babar([[-1, -5], [0, 0], [1, 5]])).not.toThrow(); + }); + + it('fits within specified width', () => { + const width = 40; + const result = babar(pts, { width, color: 'ascii' }); + // strip ANSI, check line widths + const lines = result.split('\n').filter(l => l.length > 0); + for (const line of lines) { + expect(line.length).toBeLessThanOrEqual(width + 2); // small tolerance for label overflow + } + }); + + it('throws on empty points', () => { + expect(() => babar([])).toThrow(RangeError); + }); + + it('throws on invalid color', () => { + expect(() => babar(pts, { color: 'purple' as never })).toThrow(TypeError); + }); + + it('throws on non-finite point', () => { + expect(() => babar([[NaN, 1]])).toThrow(RangeError); + }); + + it('throws on too-small height', () => { + expect(() => babar(pts, { height: 1 })).toThrow(RangeError); + }); +}); + +describe('babar partial bar resolution', () => { + it('uses sub-row block characters in color mode', () => { + // One tall bar should produce characters from the block set + const result = babar([[0, 0], [1, 10]], { color: 'cyan', width: 20, height: 10 }); + const blockChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + const hasBlock = blockChars.some(ch => result.includes(ch)); + expect(hasBlock).toBe(true); + }); +}); diff --git a/test/buckets.test.ts b/test/buckets.test.ts new file mode 100644 index 0000000..5361444 --- /dev/null +++ b/test/buckets.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { computeStats, createBuckets, avgBuckets, normalizeBuckets, bucketize } from '../src/buckets.js'; +import type { Point } from '../src/types.js'; + +const pts: Point[] = [[0, 1], [1, 5], [2, 5], [3, 1], [4, 6]]; + +describe('computeStats', () => { + it('computes correct min/max', () => { + const s = computeStats(pts); + expect(s.minX).toBe(0); + expect(s.maxX).toBe(4); + expect(s.minY).toBe(1); + expect(s.maxY).toBe(6); + expect(s.uniqueX).toBe(5); + }); + + it('counts unique X correctly with duplicates', () => { + const s = computeStats([[0, 1], [0, 2], [1, 3]]); + expect(s.uniqueX).toBe(2); + }); +}); + +describe('createBuckets', () => { + it('places points in correct buckets', () => { + const bkts = createBuckets([[0, 1], [4, 6]], 5, 0, 4); + expect(bkts[0]).toContainEqual([0, 1]); + expect(bkts[4]).toContainEqual([4, 6]); + }); + + it('handles diffX = 0 (single x value)', () => { + const bkts = createBuckets([[3, 5], [3, 7]], 3, 3, 0); + expect(bkts[0].length).toBe(2); + expect(bkts[1].length).toBe(0); + expect(bkts[2].length).toBe(0); + }); +}); + +describe('avgBuckets', () => { + it('averages values correctly', () => { + const result = avgBuckets([[[0, 2], [0, 4]], [[1, 6]]]); + expect(result[0]).toBe(3); + expect(result[1]).toBe(6); + }); + + it('carries forward previous value for empty buckets', () => { + const result = avgBuckets([[[0, 10]], [], [[2, 20]]]); + expect(result[1]).toBe(10); + }); +}); + +describe('normalizeBuckets', () => { + it('maps min→0, max→h', () => { + const result = normalizeBuckets([0, 5, 10], 0, 10, 10); + expect(result[0]).toBe(0); + expect(result[1]).toBe(5); + expect(result[2]).toBe(10); + }); + + it('returns mid-height for diff=0', () => { + const result = normalizeBuckets([5, 5, 5], 5, 0, 10); + expect(result).toEqual([5, 5, 5]); + }); +}); + +describe('bucketize', () => { + it('produces normalized bucket array of correct length', () => { + const { bkt } = bucketize(pts, 5, 0, 4, undefined, undefined, 10); + expect(bkt.length).toBe(5); + }); + + it('respects maxY override', () => { + const { max } = bucketize(pts, 5, 0, 4, undefined, 100, 10); + expect(max).toBe(100); + }); + + it('respects minY override', () => { + const { min } = bucketize(pts, 5, 0, 4, -10, undefined, 10); + expect(min).toBe(-10); + }); +}); diff --git a/test/draw.test.ts b/test/draw.test.ts new file mode 100644 index 0000000..2fb9607 --- /dev/null +++ b/test/draw.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { drawRowLabel, drawRowChart, drawRow, drawChart } from '../src/draw.js'; + +describe('drawRowLabel', () => { + it('shows label when it differs from previous row', () => { + const lblY = ['1.0', '2.0', '3.0']; + expect(drawRowLabel(1, lblY, 6)).toContain('2.0'); + }); + + it('shows empty string when label repeats', () => { + const lblY = ['1.0', '1.0', '3.0']; + expect(drawRowLabel(1, lblY, 6)).toBe(' '); + }); + + it('always shows label at row 0', () => { + const lblY = ['1.0', '1.0']; + expect(drawRowLabel(0, lblY, 6)).toContain('1.0'); + }); +}); + +describe('drawRowChart', () => { + it('renders full block for bar body in color mode', () => { + // v=5, r=2 → row is below bar top → full block + const result = drawRowChart(2, [5], 2, 'cyan', 'black'); + expect(result).toContain('█'); + }); + + it('renders horizontal grid line above bar', () => { + // v=1, r=3 → row is above bar → full cell filled with grid underscores, no separator + const result = drawRowChart(3, [1], 2, 'cyan', 'black'); + const stripped = result.replace(/\x1b\[[0-9;]*m/g, ''); + expect(stripped).toMatch(/^_+$/); + expect(stripped).not.toContain('▏'); + expect(stripped).not.toContain('█'); + }); + + it('renders X for ascii fill', () => { + const result = drawRowChart(0, [5], 2, 'ascii', 'black'); + expect(result).toContain('X'); + }); + + it('renders space for ascii above bar', () => { + const result = drawRowChart(4, [1], 2, 'ascii', 'black'); + expect(result).toBe(' '); + }); + + it('renders partial block at bar cap', () => { + // v=2.5, r=2 → partial fill (0.5 → ▄) + const result = drawRowChart(2, [2.5], 2, 'cyan', 'black'); + expect(result).toContain('▄'); + }); +}); + +describe('drawChart', () => { + it('produces correct number of lines', () => { + const lblY = ['0', '1', '2', '3', '4']; + const lines = drawChart(5, lblY, 5, [2, 4, 3], 3, 'cyan', 'black').split('\n'); + expect(lines.length).toBe(5); + }); + + it('renders top-to-bottom (last row is row 0)', () => { + const lblY = ['0.0', '1.0']; + const lines = drawChart(2, lblY, 5, [1], 3, 'ascii', 'black').split('\n'); + // row 0 (bottom) should be last line + expect(lines[lines.length - 1]).toContain('0.0'); + }); +}); diff --git a/test/validate.test.ts b/test/validate.test.ts new file mode 100644 index 0000000..da817b3 --- /dev/null +++ b/test/validate.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { validatePoints, validateOptions } from '../src/validate.js'; + +describe('validatePoints', () => { + it('throws if not an array', () => { + expect(() => validatePoints('foo')).toThrow(TypeError); + expect(() => validatePoints(null)).toThrow(TypeError); + expect(() => validatePoints(42)).toThrow(TypeError); + }); + + it('throws on empty array', () => { + expect(() => validatePoints([])).toThrow(RangeError); + }); + + it('throws if a point is not a 2-element array', () => { + expect(() => validatePoints([[1]])).toThrow(TypeError); + expect(() => validatePoints([1, 2])).toThrow(TypeError); + }); + + it('throws on non-finite values', () => { + expect(() => validatePoints([[Infinity, 1]])).toThrow(RangeError); + expect(() => validatePoints([[1, NaN]])).toThrow(RangeError); + }); + + it('accepts valid points', () => { + expect(() => validatePoints([[0, 0], [1, 1]])).not.toThrow(); + expect(() => validatePoints([[-1, -5.5]])).not.toThrow(); + }); +}); + +describe('validateOptions', () => { + it('throws on invalid color', () => { + expect(() => validateOptions({ color: 'purple' as never })).toThrow(TypeError); + }); + + it('throws on invalid grid color', () => { + expect(() => validateOptions({ grid: 'neon' as never })).toThrow(TypeError); + }); + + it('throws on width < 3', () => { + expect(() => validateOptions({ width: 2 })).toThrow(RangeError); + }); + + it('throws on height < 3', () => { + expect(() => validateOptions({ height: 1 })).toThrow(RangeError); + }); + + it('throws if minX >= maxX', () => { + expect(() => validateOptions({ minX: 5, maxX: 5 })).toThrow(RangeError); + expect(() => validateOptions({ minX: 6, maxX: 5 })).toThrow(RangeError); + }); + + it('throws if minY >= maxY', () => { + expect(() => validateOptions({ minY: 10, maxY: 5 })).toThrow(RangeError); + }); + + it('accepts valid options', () => { + expect(() => validateOptions({ color: 'cyan', grid: 'black', width: 80, height: 15 })).not.toThrow(); + expect(() => validateOptions({})).not.toThrow(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..497c387 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "lib", + "rootDir": "src", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "lib", "test"] +} From be47e9df35b4aaa91b4426235a78762fc8e6b918 Mon Sep 17 00:00:00 2001 From: Aurelien LAJOIE Date: Sat, 23 May 2026 19:00:53 +0200 Subject: [PATCH 2/4] Add aggregation strategies, log scale, reference line, categorical axis, and horizontal bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aggregation option: avg (default), sum, min, max, last, count - yScale option: linear (default) or log for exponential data - refY option: draws a ╌ threshold line at a given Y value (| in ascii mode) - Categorical X axis: accept [string, number][] points with named buckets - direction option: horizontal renders bars left-to-right with 8-level ▏▎▍▌▋▊▉█ chars - Horizontal mode supports refY as a vertical reference column - Categorical + horizontal combined: leaderboard-style charts - All features combinable (e.g. categorical + log + horizontal + refY) - 28 new tests covering all features and error cases - features.js example demonstrating all 8 feature combinations Co-Authored-By: Claude Sonnet 4.6 (1M context) --- examples/features.js | 161 +++++++++++++++++++++++++++++ src/babar.ts | 233 +++++++++++++++++++++++++++++++++++++++--- src/buckets.ts | 94 ++++++++++++++++- src/draw.ts | 98 ++++++++++++++---- src/types.ts | 11 ++ src/validate.ts | 52 +++++++--- test/buckets.test.ts | 8 +- test/features.test.ts | 191 ++++++++++++++++++++++++++++++++++ 8 files changed, 786 insertions(+), 62 deletions(-) create mode 100644 examples/features.js create mode 100644 test/features.test.ts diff --git a/examples/features.js b/examples/features.js new file mode 100644 index 0000000..bb84539 --- /dev/null +++ b/examples/features.js @@ -0,0 +1,161 @@ +var babar = require('../lib/babar'); + +function section(title) { + var w = 72; + var inner = title.length + 2; + var pad = Math.max(0, Math.floor((w - inner) / 2)); + var right = Math.max(0, w - inner - pad); + console.log('\n' + '─'.repeat(pad) + ' ' + title + ' ' + '─'.repeat(right)); +} + +// ───────────────────────────────────────────────────────────────────────────── +section('1. aggregation: avg (default) vs sum vs count'); + +var requestLog = []; +for (var i = 0; i < 10; i++) { + for (var j = 0; j < Math.floor(Math.random() * 5 + 1); j++) { + requestLog.push([i, Math.random() * 200 + 50]); // response times + } +} + +console.log(babar(requestLog, { + caption: 'avg response time per second', + color: 'cyan', grid: 'grey', width: 72, height: 10, + aggregation: 'avg', yFractions: 0, +})); +console.log(babar(requestLog, { + caption: 'total response time (sum)', + color: 'magenta', grid: 'grey', width: 72, height: 10, + aggregation: 'sum', yFractions: 0, +})); +console.log(babar(requestLog, { + caption: 'request count per second', + color: 'green', grid: 'grey', width: 72, height: 8, + aggregation: 'count', yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('2. aggregation: min / max — shows spread'); + +var noisyPts = []; +for (var i = 0; i < 12; i++) { + for (var j = 0; j < 8; j++) { + noisyPts.push([i, 50 + Math.sin(i * 0.5) * 30 + (Math.random() - 0.5) * 40]); + } +} +console.log(babar(noisyPts, { + caption: 'worst case (max) latency per bucket', + color: 'red', grid: 'grey', width: 72, height: 10, + aggregation: 'max', yFractions: 0, +})); +console.log(babar(noisyPts, { + caption: 'best case (min) latency per bucket', + color: 'green', grid: 'grey', width: 72, height: 10, + aggregation: 'min', yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('3. yScale: log — exponential data readable at a glance'); + +var expPts = []; +for (var i = 1; i <= 20; i++) expPts.push([i, Math.pow(2, i)]); + +console.log(babar(expPts, { + caption: '2^x linear scale — everything looks flat except the last bar', + color: 'yellow', grid: 'grey', width: 72, height: 12, + yScale: 'linear', yFractions: 0, +})); +console.log(babar(expPts, { + caption: '2^x log scale — growth is visible across all values', + color: 'cyan', grid: 'grey', width: 72, height: 12, + yScale: 'log', yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('4. refY — reference / threshold line'); + +var latency = [ + [0,45],[1,62],[2,38],[3,91],[4,110],[5,78],[6,55],[7,130],[8,48],[9,72], +]; +console.log(babar(latency, { + caption: 'p99 latency (ms) — SLA threshold at 80ms', + color: 'cyan', grid: 'grey', width: 72, height: 12, + refY: 80, yFractions: 0, +})); + +// refY with log scale +console.log(babar(expPts, { + caption: '2^x log scale with refY=1000', + color: 'magenta', grid: 'grey', width: 72, height: 12, + yScale: 'log', yFractions: 0, refY: 1000, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('5. categorical X — named buckets (vertical)'); + +var monthlyRevenue = [ + ['Jan', 12400], ['Feb', 9800], ['Mar', 15600], ['Apr', 18200], + ['May', 21000], ['Jun', 19500], ['Jul', 17800], ['Aug', 22400], + ['Sep', 20100], ['Oct', 24600], ['Nov', 28900], ['Dec', 31200], +]; +console.log(babar(monthlyRevenue, { + caption: 'monthly revenue ($)', + color: 'green', grid: 'grey', width: 72, height: 14, + yFractions: 0, +})); + +// categorical with aggregation on duplicate labels +var surveyResults = [ + ['Strongly agree', 34], ['Agree', 28], ['Neutral', 15], + ['Disagree', 12], ['Strongly disagree', 8], + // second survey round, same categories + ['Strongly agree', 40], ['Agree', 31], ['Neutral', 11], + ['Disagree', 9], ['Strongly disagree', 5], +]; +console.log(babar(surveyResults, { + caption: 'survey results (avg of 2 rounds)', + color: 'blue', grid: 'grey', width: 72, height: 12, + aggregation: 'avg', yFractions: 0, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('6. categorical X — horizontal direction'); + +console.log(babar(monthlyRevenue, { + caption: 'monthly revenue — horizontal', + color: 'green', grid: 'grey', width: 72, + direction: 'horizontal', yFractions: 0, +})); + +console.log(babar(surveyResults, { + caption: 'survey results — horizontal, avg aggregation', + color: 'cyan', grid: 'grey', width: 72, + direction: 'horizontal', aggregation: 'avg', yFractions: 1, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('7. numeric horizontal — time series turned sideways'); + +var cpuLoad = []; +for (var i = 0; i < 30; i++) { + cpuLoad.push([i, 20 + Math.sin(i * 0.4) * 15 + Math.random() * 10]); +} +console.log(babar(cpuLoad, { + caption: 'CPU load % over 30s (horizontal)', + color: 'yellow', grid: 'grey', width: 72, height: 20, + direction: 'horizontal', yFractions: 1, refY: 30, +})); + +// ───────────────────────────────────────────────────────────────────────────── +section('8. combining features: log + refY + categorical + horizontal'); + +var diskUsage = [ + ['/', 45000], ['/home', 320000], ['/var', 8500], + ['/tmp', 120], ['/boot', 980], ['/srv', 62000], +]; +console.log(babar(diskUsage, { + caption: 'disk usage (MB) — log scale, 10000 MB threshold', + color: 'red', grid: 'grey', width: 72, + direction: 'horizontal', yScale: 'log', + refY: 10000, yFractions: 0, +})); diff --git a/src/babar.ts b/src/babar.ts index b16ab95..3167148 100644 --- a/src/babar.ts +++ b/src/babar.ts @@ -1,10 +1,11 @@ import chalk from 'chalk'; import { validatePoints, validateOptions } from './validate.js'; -import { computeStats, bucketize } from './buckets.js'; -import { drawChart } from './draw.js'; -import type { Point, Options, Color } from './types.js'; +import { computeStats, bucketize, bucketizeCategorical } from './buckets.js'; +import { drawChart, drawHorizontalChart } from './draw.js'; +import type { AnyPoint, Point, Options, Color } from './types.js'; -export type { Point, Options, Color }; +export type { AnyPoint, Point, Options, Color }; +export type { Aggregation, YScale, Direction, CategoricalPoint } from './types.js'; function clamp(min: number, max: number, val: number): number { return Math.max(min, Math.min(max, val)); @@ -15,16 +16,32 @@ function repeat(n: number, ch: string): string { return ch.repeat(n); } -export default function babar(points: Point[], options: Options = {}): string { +function isCategorical(points: AnyPoint[]): boolean { + return typeof points[0][0] === 'string'; +} + +export default function babar(points: AnyPoint[], options: Options = {}): string { validatePoints(points); validateOptions(options); const color: Color = options.color ?? 'cyan'; const grid: Color = options.grid ?? 'black'; + const aggregation = options.aggregation ?? 'avg'; + const yScale = options.yScale ?? 'linear'; + const direction = options.direction ?? 'vertical'; let width = options.width ?? 80; let height = options.height ?? 15; + const caption = options.caption; + + // ── Categorical path ──────────────────────────────────────────────────────── + if (isCategorical(points)) { + return renderCategorical(points, options, color, grid, aggregation, yScale, direction, width, height, caption); + } - const stats = computeStats(points); + // ── Numeric path ──────────────────────────────────────────────────────────── + const numPoints = points as Point[]; + + const stats = computeStats(numPoints); const minX = options.minX ?? stats.minX; const maxX = options.maxX ?? stats.maxX; const minY = options.minY; @@ -33,15 +50,23 @@ export default function babar(points: Point[], options: Options = {}): string { const diffX = maxX - minX; const diffY = (maxY ?? stats.maxY) - (minY ?? stats.minY); - const caption = options.caption; height -= 1 + (caption !== undefined ? 1 : 0); - const yFractions = options.yFractions ?? - clamp(0, 8, Math.log(height / (diffY || 1) * 5) / Math.LN10); - const effectiveMinY = minY ?? stats.minY; const effectiveMaxY = maxY ?? stats.maxY; + if (direction === 'horizontal') { + return renderHorizontalNumeric( + numPoints, options, color, grid, aggregation, yScale, + width, height, caption, + minX, maxX, diffX, stats, + effectiveMinY, effectiveMaxY, diffY, + ); + } + + const yFractions = options.yFractions ?? + clamp(0, 8, Math.log(height / (diffY || 1) * 5) / Math.LN10); + const lblYW = 1 + Math.max( effectiveMinY.toFixed(yFractions).length, effectiveMaxY.toFixed(yFractions).length, @@ -56,22 +81,36 @@ export default function babar(points: Point[], options: Options = {}): string { clamp(0, 8, Math.log(numBkts / (diffX || 1) * 5) / Math.LN10); const { bkt, min, max, diff } = bucketize( - points, numBkts, minX, diffX, minY, maxY, height, + numPoints, numBkts, minX, diffX, minY, maxY, height, aggregation, yScale, ); - // Build Y axis labels (bottom to top) + // Y axis labels const lblY: string[] = []; for (let v = height - 1; v >= 0; v--) { - lblY.unshift((min + diff * v / Math.max(1, height - 1)).toFixed(yFractions)); + const raw = yScale === 'log' + ? Math.exp(Math.log(Math.max(min, Number.EPSILON)) + Math.log(Math.max(max, Number.EPSILON) / Math.max(min, Number.EPSILON)) * v / Math.max(1, height - 1)) + : min + diff * v / Math.max(1, height - 1); + lblY.unshift(raw.toFixed(yFractions)); } - // Compute X label density + // refY → normalized row position + let refRow: number | undefined; + if (options.refY !== undefined) { + if (yScale === 'log') { + const logMin = Math.log(Math.max(min, Number.EPSILON)); + const logMax = Math.log(Math.max(max, Number.EPSILON)); + refRow = (Math.log(Math.max(options.refY, Number.EPSILON)) - logMin) / (logMax - logMin) * (height - 1); + } else { + refRow = (options.refY - min) / (diff || 1) * (height - 1); + } + } + + // X label density let lblXW = 0; for (let u = 0; u < numBkts; u++) { const lbl = (minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions); lblXW = Math.max(lblXW, lbl.length); } - let lblXN = numBkts; let lblXI = 1; while ((lblXN + 1) * lblXW >= numBkts * bktW) { @@ -80,14 +119,13 @@ export default function babar(points: Point[], options: Options = {}): string { } let out = ''; - if (caption !== undefined) { out += repeat(lblYW, ' '); out += color === 'ascii' ? caption : chalk.bold(caption); out += '\n'; } - out += drawChart(height, lblY, lblYW, bkt, bktW, color, grid) + '\n'; + out += drawChart(height, lblY, lblYW, bkt, bktW, color, grid, refRow) + '\n'; out += repeat(lblYW, ' '); for (let x = 0; x < lblXN; x++) { @@ -100,6 +138,167 @@ export default function babar(points: Point[], options: Options = {}): string { return out; } +// ─── Categorical renderer ──────────────────────────────────────────────────── + +function renderCategorical( + points: AnyPoint[], + options: Options, + color: Color, + grid: Color, + aggregation: import('./types.js').Aggregation, + yScale: import('./types.js').YScale, + direction: import('./types.js').Direction, + width: number, + height: number, + caption: string | undefined, +): string { + const minY = options.minY; + const maxY = options.maxY; + const innerH = height - 1 - (caption !== undefined ? 1 : 0); + + const { bkt, labels, min, max, diff } = bucketizeCategorical( + points, aggregation, yScale, minY, maxY, innerH, + ); + + const yFractions = options.yFractions ?? 1; + const numBkts = bkt.length; + + if (direction === 'horizontal') { + const maxLabelW = Math.max(...labels.map(l => l.length)); + const barAreaW = width - maxLabelW - 2 - 8; // 2 for spacing, 8 for value label + const normalizedBkt = bkt; // already normalized to innerH + + // Re-normalize to barAreaW + const barWidths = bkt.map(v => v / (innerH || 1) * Math.max(1, barAreaW)); + + const valLabels = bkt.map((_, i) => { + const rawVal = yScale === 'log' + ? Math.exp(Math.log(Math.max(min, Number.EPSILON)) + bkt[i] / (innerH || 1) * Math.log(Math.max(max, Number.EPSILON) / Math.max(min, Number.EPSILON))) + : min + (diff || 0) * bkt[i] / (innerH || 1); + return rawVal.toFixed(yFractions); + }); + + let out = ''; + if (caption !== undefined) { + out += color === 'ascii' ? caption : chalk.bold(caption); + out += '\n'; + } + out += drawHorizontalChart(barWidths, labels, maxLabelW, barAreaW, color, grid, valLabels); + return out; + } + + // Vertical categorical + const lblYW = 1 + Math.max( + (min).toFixed(yFractions).length, + (max).toFixed(yFractions).length, + ); + const availWidth = width - lblYW; + const bktW = Math.max(1, Math.floor(availWidth / numBkts)); + + const lblY: string[] = []; + for (let v = innerH - 1; v >= 0; v--) { + const raw = yScale === 'log' + ? Math.exp(Math.log(Math.max(min, Number.EPSILON)) + Math.log(Math.max(max, Number.EPSILON) / Math.max(min, Number.EPSILON)) * v / Math.max(1, innerH - 1)) + : min + diff * v / Math.max(1, innerH - 1); + lblY.unshift(raw.toFixed(yFractions)); + } + + let refRow: number | undefined; + if (options.refY !== undefined) { + refRow = (options.refY - min) / (diff || 1) * (innerH - 1); + } + + let out = ''; + if (caption !== undefined) { + out += repeat(lblYW, ' '); + out += color === 'ascii' ? caption : chalk.bold(caption); + out += '\n'; + } + + out += drawChart(innerH, lblY, lblYW, bkt, bktW, color, grid, refRow) + '\n'; + out += repeat(lblYW, ' '); + + // X labels: category names, thinned if they overlap + const maxLblW = Math.max(...labels.map(l => l.length)); + let step = 1; + while (step < numBkts && (numBkts / step) * (maxLblW + 1) > numBkts * bktW) step *= 2; + + for (let i = 0; i < numBkts; i += step) { + const lbl = labels[i].substring(0, bktW * step); + out += lbl; + out += repeat(bktW * step - lbl.length, ' '); + } + + return out; +} + +// ─── Horizontal numeric renderer ───────────────────────────────────────────── + +function renderHorizontalNumeric( + points: Point[], + options: Options, + color: Color, + grid: Color, + aggregation: import('./types.js').Aggregation, + yScale: import('./types.js').YScale, + width: number, + height: number, + caption: string | undefined, + minX: number, + maxX: number, + diffX: number, + stats: import('./buckets.js').PointStats, + effectiveMinY: number, + effectiveMaxY: number, + diffY: number, +): string { + const yFractions = options.yFractions ?? 1; + const xFractions = options.xFractions ?? 0; + + const maxLabelW = (minX).toFixed(xFractions).length; + const barAreaW = width - maxLabelW - 2 - (effectiveMaxY.toFixed(yFractions).length + 1); + + const numBkts = Math.max(1, Math.min(stats.uniqueX, height)); + + const { bkt, min, max, diff } = bucketize( + points, numBkts, minX, diffX, options.minY, options.maxY, barAreaW + 1, aggregation, yScale, + ); + + // refY → normalized column + let refCol: number | undefined; + if (options.refY !== undefined) { + if (yScale === 'log') { + const logMin = Math.log(Math.max(min, Number.EPSILON)); + const logMax = Math.log(Math.max(max, Number.EPSILON)); + refCol = (Math.log(Math.max(options.refY, Number.EPSILON)) - logMin) / (logMax - logMin) * barAreaW; + } else { + refCol = (options.refY - min) / (diff || 1) * barAreaW; + } + } + + // X-axis labels (one per bucket row) + const labels: string[] = []; + for (let u = 0; u < numBkts; u++) { + labels.push((minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions)); + } + + // Value labels per bucket + const valLabels = bkt.map(v => { + const raw = yScale === 'log' + ? Math.exp(Math.log(Math.max(min, Number.EPSILON)) + v / barAreaW * Math.log(Math.max(max, Number.EPSILON) / Math.max(min, Number.EPSILON))) + : min + diff * v / barAreaW; + return raw.toFixed(yFractions); + }); + + let out = ''; + if (caption !== undefined) { + out += color === 'ascii' ? caption : chalk.bold(caption); + out += '\n'; + } + out += drawHorizontalChart(bkt, labels, maxLabelW, barAreaW, color, grid, valLabels, refCol); + return out; +} + // Allow `require('babar')` to work directly without `.default` if (typeof module !== 'undefined') { module.exports = babar; diff --git a/src/buckets.ts b/src/buckets.ts index 4f18639..eb341aa 100644 --- a/src/buckets.ts +++ b/src/buckets.ts @@ -1,4 +1,4 @@ -import type { Point } from './types.js'; +import type { AnyPoint, Point, Aggregation } from './types.js'; export interface PointStats { minX: number; @@ -42,13 +42,32 @@ export function createBuckets( return bkt; } -export function avgBuckets(bkt: Point[][]): number[] { +export function aggregateBuckets(bkt: Point[][], aggregation: Aggregation): number[] { const result: number[] = []; let prev = 0; for (const values of bkt) { if (values.length > 0) { - prev = values.reduce((sum, p) => sum + p[1], 0) / values.length; + switch (aggregation) { + case 'avg': + prev = values.reduce((s, p) => s + p[1], 0) / values.length; + break; + case 'sum': + prev = values.reduce((s, p) => s + p[1], 0); + break; + case 'min': + prev = Math.min(...values.map(p => p[1])); + break; + case 'max': + prev = Math.max(...values.map(p => p[1])); + break; + case 'last': + prev = values[values.length - 1][1]; + break; + case 'count': + prev = values.length; + break; + } } result.push(prev); } @@ -61,6 +80,17 @@ export function normalizeBuckets(bkt: number[], min: number, diff: number, h: nu return bkt.map(v => (v - min) / diff * h); } +export function normalizeBucketsLog(bkt: number[], min: number, max: number, h: number): number[] { + const logMin = Math.log(Math.max(min, Number.EPSILON)); + const logMax = Math.log(Math.max(max, Number.EPSILON)); + const logDiff = logMax - logMin; + if (logDiff === 0) return bkt.map(() => h / 2); + return bkt.map(v => { + const logV = Math.log(Math.max(v, Number.EPSILON)); + return (logV - logMin) / logDiff * h; + }); +} + export interface BucketizeResult { bkt: number[]; min: number; @@ -76,8 +106,57 @@ export function bucketize( minY: number | undefined, maxY: number | undefined, h: number, + aggregation: Aggregation = 'avg', + yScale: 'linear' | 'log' = 'linear', ): BucketizeResult { - const raw = avgBuckets(createBuckets(points, numBkts, minX, diffX)); + const raw = aggregateBuckets(createBuckets(points, numBkts, minX, diffX), aggregation); + let min = Math.min(...raw); + let max = Math.max(...raw); + + if (maxY !== undefined) max = maxY; + if (minY !== undefined) min = minY; + + const diff = max - min; + + const normalized = yScale === 'log' + ? normalizeBucketsLog(raw, min, max, h - 1) + : normalizeBuckets(raw, min, diff, h - 1); + + return { bkt: normalized, min, max, diff }; +} + +// ─── Categorical support ───────────────────────────────────────────────────── + +export interface CategoricalBucketizeResult { + bkt: number[]; + labels: string[]; + min: number; + max: number; + diff: number; +} + +export function bucketizeCategorical( + points: AnyPoint[], + aggregation: Aggregation = 'avg', + yScale: 'linear' | 'log' = 'linear', + minY?: number, + maxY?: number, + h: number = 14, +): CategoricalBucketizeResult { + // Group by label in insertion order + const groups = new Map(); + for (const [x, y] of points) { + const key = String(x); + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(y as number); + } + + const labels = [...groups.keys()]; + const rawBuckets: Point[][] = labels.map(lbl => + groups.get(lbl)!.map(y => [0, y] as Point) + ); + + const raw = aggregateBuckets(rawBuckets, aggregation); let min = Math.min(...raw); let max = Math.max(...raw); @@ -85,5 +164,10 @@ export function bucketize( if (minY !== undefined) min = minY; const diff = max - min; - return { bkt: normalizeBuckets(raw, min, diff, h - 1), min, max, diff }; + + const normalized = yScale === 'log' + ? normalizeBucketsLog(raw, min, max, h - 1) + : normalizeBuckets(raw, min, diff, h - 1); + + return { bkt: normalized, labels, min, max, diff }; } diff --git a/src/draw.ts b/src/draw.ts index bad48c6..fee640d 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -4,6 +4,8 @@ import type { Color } from './types.js'; // 8-level vertical block characters: index 0 = empty, 8 = full block const BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] as const; const BLOCKS_ASCII_FILL = 'X'; +const REF_CHAR = '╌'; +const REF_CHAR_ASCII = '-'; function repeat(n: number, ch: string): string { if (n <= 0) return ''; @@ -16,30 +18,14 @@ function colorize(str: string, color: Color): string { return fn ? fn(str) : str; } -/** - * Returns the block character for a fractional fill level [0..1]. - * 0 → empty, 1 → full █ - */ function blockChar(fraction: number): string { const idx = Math.round(fraction * 8); return BLOCKS[Math.min(8, Math.max(0, idx))]; } -/** - * Render one column cell at row `r` for a bar of normalized height `v`. - * `v` is in [0..height-1] coordinate space (floats). - * - * Row 0 is the bottom, row (height-1) is the top. - * For each row: - * - row < floor(v) → full block (bar body) - * - row = floor(v) → partial block (bar cap, fractional part of v) - * - row > floor(v) → empty (above bar) - */ function cellChar(r: number, v: number, ascii: boolean): string { const full = Math.floor(v); - if (r < full) { - return ascii ? BLOCKS_ASCII_FILL : '█'; - } + if (r < full) return ascii ? BLOCKS_ASCII_FILL : '█'; if (r === full) { const frac = v - full; if (ascii) return frac >= 0.5 ? BLOCKS_ASCII_FILL : ' '; @@ -54,18 +40,26 @@ export function drawRowChart( bktW: number, color: Color, grid: Color, + refRow?: number, ): string { const ascii = color === 'ascii'; + const isRefRow = refRow !== undefined && r === Math.round(refRow); let out = ''; for (const v of bkt) { const ch = cellChar(r, v, ascii); if (ch === ' ') { - // above bar: full cell is horizontal grid line - out += ascii ? repeat(bktW, ' ') : colorize(repeat(bktW, '_'), grid); + if (isRefRow) { + // reference line overlaid on empty space + const refCh = ascii ? REF_CHAR_ASCII : colorize(REF_CHAR, grid); + out += repeat(bktW, refCh); + } else { + // normal horizontal grid line + out += ascii ? repeat(bktW, ' ') : colorize(repeat(bktW, '_'), grid); + } } else { - // bar body or cap: colored fill, right edge is a grid underscore + // bar body or cap const bodyW = Math.max(1, bktW - 1); const colored = ascii ? repeat(bodyW, ch) : colorize(repeat(bodyW, ch), color); const edge = bktW > 1 ? (ascii ? ' ' : colorize('_', grid)) : ''; @@ -89,8 +83,9 @@ export function drawRow( bktW: number, color: Color, grid: Color, + refRow?: number, ): string { - return `${drawRowLabel(r, lblY, lblYW)} ${drawRowChart(r, bkt, bktW, color, grid)}`; + return `${drawRowLabel(r, lblY, lblYW)} ${drawRowChart(r, bkt, bktW, color, grid, refRow)}`; } export function drawChart( @@ -101,10 +96,69 @@ export function drawChart( bktW: number, color: Color, grid: Color, + refRow?: number, ): string { const rows: string[] = []; for (let r = h - 1; r >= 0; r--) { - rows.push(drawRow(r, lblY, lblYW, bkt, bktW, color, grid)); + rows.push(drawRow(r, lblY, lblYW, bkt, bktW, color, grid, refRow)); + } + return rows.join('\n'); +} + +// ─── Horizontal bar chart ──────────────────────────────────────────────────── + +const H_BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'] as const; + +function hBlockChar(fraction: number): string { + const idx = Math.round(fraction * 8); + return H_BLOCKS[Math.min(8, Math.max(0, idx))]; +} + +function hCellChar(c: number, v: number, ascii: boolean): string { + const full = Math.floor(v); + if (c < full) return ascii ? BLOCKS_ASCII_FILL : '█'; + if (c === full) { + const frac = v - full; + if (ascii) return frac >= 0.5 ? BLOCKS_ASCII_FILL : ' '; + return hBlockChar(frac); + } + return ' '; +} + +export function drawHorizontalChart( + bkt: number[], + labels: string[], + maxLabelW: number, + barAreaW: number, + color: Color, + grid: Color, + lblY: string[], // value labels per bucket (for right-side annotation) + refCol?: number, // normalized column position of refY +): string { + const ascii = color === 'ascii'; + const rows: string[] = []; + + for (let i = 0; i < bkt.length; i++) { + const v = bkt[i]; + const lbl = (labels[i] ?? String(i)).padStart(maxLabelW); + let bar = ''; + + for (let c = 0; c < barAreaW; c++) { + const ch = hCellChar(c, v, ascii); + if (ch === ' ') { + if (refCol !== undefined && c === Math.round(refCol)) { + bar += ascii ? '|' : colorize('╎', grid); + } else { + bar += ascii ? '.' : colorize('·', grid); + } + } else { + bar += ascii ? ch : colorize(ch, color); + } + } + + const valLbl = ` ${lblY[i] ?? ''}`; + rows.push(`${lbl} ${bar}${valLbl}`); } + return rows.join('\n'); } diff --git a/src/types.ts b/src/types.ts index 3ad33db..8f2d28e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,6 @@ export type Point = [number, number]; +export type CategoricalPoint = [string, number]; +export type AnyPoint = Point | CategoricalPoint; export type Color = | 'ascii' @@ -13,6 +15,10 @@ export type Color = | 'grey' | 'gray'; +export type Aggregation = 'avg' | 'sum' | 'min' | 'max' | 'last' | 'count'; +export type YScale = 'linear' | 'log'; +export type Direction = 'vertical' | 'horizontal'; + export interface Options { caption?: string; color?: Color; @@ -25,4 +31,9 @@ export interface Options { maxX?: number; minY?: number; maxY?: number; + // new options + aggregation?: Aggregation; + yScale?: YScale; + refY?: number; + direction?: Direction; } diff --git a/src/validate.ts b/src/validate.ts index 9dade57..7b7d5be 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -1,24 +1,35 @@ -import type { Point, Options, Color } from './types.js'; +import type { AnyPoint, Options, Color, Aggregation, YScale, Direction } from './types.js'; const VALID_COLORS: Color[] = [ 'ascii', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'grey', 'gray', ]; -export function validatePoints(points: unknown): asserts points is Point[] { +const VALID_AGGREGATIONS: Aggregation[] = ['avg', 'sum', 'min', 'max', 'last', 'count']; +const VALID_YSCALES: YScale[] = ['linear', 'log']; +const VALID_DIRECTIONS: Direction[] = ['vertical', 'horizontal']; + +export function validatePoints(points: unknown): asserts points is AnyPoint[] { if (!Array.isArray(points)) { throw new TypeError('babar: points must be an array'); } if (points.length === 0) { throw new RangeError('babar: points array must not be empty'); } + const firstType = typeof points[0]?.[0]; for (let i = 0; i < points.length; i++) { const p = points[i]; if (!Array.isArray(p) || p.length < 2) { throw new TypeError(`babar: points[${i}] must be a [x, y] array`); } - if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) { - throw new RangeError(`babar: points[${i}] contains non-finite value`); + if (typeof p[0] !== firstType) { + throw new TypeError(`babar: all points must have the same x type (mixed string/number)`); + } + if (typeof p[0] === 'number' && !Number.isFinite(p[0])) { + throw new RangeError(`babar: points[${i}] x value is non-finite`); + } + if (!Number.isFinite(p[1])) { + throw new RangeError(`babar: points[${i}] y value is non-finite`); } } } @@ -40,18 +51,31 @@ export function validateOptions(opts: Options): void { if (opts.height !== undefined && opts.height < 3) { throw new RangeError('babar: height must be at least 3'); } - if ( - opts.minX !== undefined && - opts.maxX !== undefined && - opts.minX >= opts.maxX - ) { + if (opts.minX !== undefined && opts.maxX !== undefined && opts.minX >= opts.maxX) { throw new RangeError('babar: minX must be less than maxX'); } - if ( - opts.minY !== undefined && - opts.maxY !== undefined && - opts.minY >= opts.maxY - ) { + if (opts.minY !== undefined && opts.maxY !== undefined && opts.minY >= opts.maxY) { throw new RangeError('babar: minY must be less than maxY'); } + if (opts.aggregation !== undefined && !VALID_AGGREGATIONS.includes(opts.aggregation)) { + throw new TypeError( + `babar: invalid aggregation "${opts.aggregation}". Valid values: ${VALID_AGGREGATIONS.join(', ')}` + ); + } + if (opts.yScale !== undefined && !VALID_YSCALES.includes(opts.yScale)) { + throw new TypeError( + `babar: invalid yScale "${opts.yScale}". Valid values: ${VALID_YSCALES.join(', ')}` + ); + } + if (opts.yScale === 'log' && opts.minY !== undefined && opts.minY <= 0) { + throw new RangeError('babar: minY must be > 0 when yScale is "log"'); + } + if (opts.direction !== undefined && !VALID_DIRECTIONS.includes(opts.direction)) { + throw new TypeError( + `babar: invalid direction "${opts.direction}". Valid values: ${VALID_DIRECTIONS.join(', ')}` + ); + } + if (opts.refY !== undefined && !Number.isFinite(opts.refY)) { + throw new RangeError('babar: refY must be a finite number'); + } } diff --git a/test/buckets.test.ts b/test/buckets.test.ts index 5361444..da438c8 100644 --- a/test/buckets.test.ts +++ b/test/buckets.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { computeStats, createBuckets, avgBuckets, normalizeBuckets, bucketize } from '../src/buckets.js'; +import { computeStats, createBuckets, aggregateBuckets, normalizeBuckets, bucketize } from '../src/buckets.js'; import type { Point } from '../src/types.js'; const pts: Point[] = [[0, 1], [1, 5], [2, 5], [3, 1], [4, 6]]; @@ -35,15 +35,15 @@ describe('createBuckets', () => { }); }); -describe('avgBuckets', () => { +describe('aggregateBuckets', () => { it('averages values correctly', () => { - const result = avgBuckets([[[0, 2], [0, 4]], [[1, 6]]]); + const result = aggregateBuckets([[[0, 2], [0, 4]], [[1, 6]]], 'avg'); expect(result[0]).toBe(3); expect(result[1]).toBe(6); }); it('carries forward previous value for empty buckets', () => { - const result = avgBuckets([[[0, 10]], [], [[2, 20]]]); + const result = aggregateBuckets([[[0, 10]], [], [[2, 20]]], 'avg'); expect(result[1]).toBe(10); }); }); diff --git a/test/features.test.ts b/test/features.test.ts new file mode 100644 index 0000000..c7878d6 --- /dev/null +++ b/test/features.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect } from 'vitest'; +import babar from '../src/babar.js'; +import { aggregateBuckets } from '../src/buckets.js'; +import type { Point, CategoricalPoint } from '../src/types.js'; + +const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ''); + +// ─── Aggregation strategies ─────────────────────────────────────────────────── + +describe('aggregation strategies', () => { + const bkt: Point[][] = [ + [[0, 2], [0, 4], [0, 6]], // avg=4, sum=12, min=2, max=6, last=6, count=3 + [[1, 10]], // avg=10, sum=10, min=10, max=10, last=10, count=1 + ]; + + it('avg', () => { + const r = aggregateBuckets(bkt, 'avg'); + expect(r[0]).toBeCloseTo(4); + expect(r[1]).toBeCloseTo(10); + }); + + it('sum', () => { + const r = aggregateBuckets(bkt, 'sum'); + expect(r[0]).toBeCloseTo(12); + expect(r[1]).toBeCloseTo(10); + }); + + it('min', () => { + const r = aggregateBuckets(bkt, 'min'); + expect(r[0]).toBe(2); + expect(r[1]).toBe(10); + }); + + it('max', () => { + const r = aggregateBuckets(bkt, 'max'); + expect(r[0]).toBe(6); + expect(r[1]).toBe(10); + }); + + it('last', () => { + const r = aggregateBuckets(bkt, 'last'); + expect(r[0]).toBe(6); + expect(r[1]).toBe(10); + }); + + it('count', () => { + const r = aggregateBuckets(bkt, 'count'); + expect(r[0]).toBe(3); + expect(r[1]).toBe(1); + }); + + it('babar respects aggregation option', () => { + const pts: Point[] = [[0, 1], [0, 9], [1, 5]]; + const sumResult = strip(babar(pts, { aggregation: 'sum', color: 'ascii', width: 20, height: 5 })); + const avgResult = strip(babar(pts, { aggregation: 'avg', color: 'ascii', width: 20, height: 5 })); + expect(sumResult).not.toBe(avgResult); + }); + + it('throws on invalid aggregation', () => { + expect(() => babar([[0, 1]], { aggregation: 'median' as never })).toThrow(TypeError); + }); +}); + +// ─── Log scale ──────────────────────────────────────────────────────────────── + +describe('yScale: log', () => { + const pts: Point[] = [[0, 1], [1, 10], [2, 100], [3, 1000]]; + + it('renders without throwing', () => { + expect(() => babar(pts, { yScale: 'log', color: 'ascii' })).not.toThrow(); + }); + + it('produces different output than linear for exponential data', () => { + const linear = strip(babar(pts, { yScale: 'linear', color: 'ascii', height: 8 })); + const log = strip(babar(pts, { yScale: 'log', color: 'ascii', height: 8 })); + expect(linear).not.toBe(log); + }); + + it('throws when minY <= 0', () => { + expect(() => babar(pts, { yScale: 'log', minY: 0 })).toThrow(RangeError); + expect(() => babar(pts, { yScale: 'log', minY: -1 })).toThrow(RangeError); + }); + + it('throws on invalid yScale value', () => { + expect(() => babar(pts, { yScale: 'sqrt' as never })).toThrow(TypeError); + }); +}); + +// ─── Reference line ─────────────────────────────────────────────────────────── + +describe('refY reference line', () => { + const pts: Point[] = [[0, 0], [1, 5], [2, 10]]; + + it('renders without throwing', () => { + expect(() => babar(pts, { refY: 5, color: 'ascii' })).not.toThrow(); + }); + + it('includes dashes in ascii mode at ref row', () => { + const result = strip(babar(pts, { refY: 5, color: 'ascii', height: 8, width: 40 })); + expect(result).toContain('-'); + }); + + it('produces different output than without refY', () => { + const without = strip(babar(pts, { color: 'ascii', height: 10, width: 40 })); + const withRef = strip(babar(pts, { refY: 5, color: 'ascii', height: 10, width: 40 })); + expect(without).not.toBe(withRef); + }); + + it('throws on non-finite refY', () => { + expect(() => babar(pts, { refY: NaN })).toThrow(RangeError); + expect(() => babar(pts, { refY: Infinity })).toThrow(RangeError); + }); + + it('works with color mode (╌ character)', () => { + const result = babar(pts, { refY: 5, color: 'cyan', height: 8, width: 40 }); + expect(strip(result)).toContain('╌'); + }); +}); + +// ─── Categorical X axis ─────────────────────────────────────────────────────── + +describe('categorical x axis', () => { + const pts: CategoricalPoint[] = [ + ['Jan', 10], ['Feb', 25], ['Mar', 15], ['Apr', 40], ['May', 30], + ]; + + it('renders without throwing', () => { + expect(() => babar(pts, { color: 'ascii' })).not.toThrow(); + }); + + it('includes category names in output', () => { + const result = strip(babar(pts, { color: 'ascii', width: 60, height: 10 })); + expect(result).toContain('Jan'); + expect(result).toContain('Apr'); + }); + + it('works with aggregation on duplicate labels', () => { + const dup: CategoricalPoint[] = [['A', 10], ['A', 20], ['B', 5]]; + const avg = strip(babar(dup, { aggregation: 'avg', color: 'ascii' })); + const sum = strip(babar(dup, { aggregation: 'sum', color: 'ascii' })); + expect(avg).not.toBe(sum); + }); + + it('throws on mixed string/number x types', () => { + expect(() => babar([['A', 1], [2, 3]] as never)).toThrow(TypeError); + }); + + it('works horizontally', () => { + const result = strip(babar(pts, { direction: 'horizontal', color: 'ascii', width: 60 })); + expect(result).toContain('Jan'); + expect(result).toContain('X'); // ascii fill char + }); +}); + +// ─── Horizontal bar chart ───────────────────────────────────────────────────── + +describe('direction: horizontal', () => { + const pts: Point[] = [[0, 10], [1, 50], [2, 30], [3, 80], [4, 20]]; + + it('renders without throwing', () => { + expect(() => babar(pts, { direction: 'horizontal', color: 'ascii' })).not.toThrow(); + }); + + it('uses horizontal block characters in color mode', () => { + const result = babar(pts, { direction: 'horizontal', color: 'cyan', width: 50, height: 10 }); + const blockChars = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; + const stripped = strip(result); + const hasBlock = blockChars.some(ch => stripped.includes(ch)); + expect(hasBlock).toBe(true); + }); + + it('produces different layout from vertical', () => { + const vert = strip(babar(pts, { direction: 'vertical', color: 'ascii', width: 50, height: 10 })); + const horiz = strip(babar(pts, { direction: 'horizontal', color: 'ascii', width: 50, height: 10 })); + expect(vert).not.toBe(horiz); + }); + + it('includes a caption when provided', () => { + const result = strip(babar(pts, { direction: 'horizontal', caption: 'My H Chart', color: 'ascii' })); + expect(result).toContain('My H Chart'); + }); + + it('throws on invalid direction', () => { + expect(() => babar(pts, { direction: 'diagonal' as never })).toThrow(TypeError); + }); + + it('refY renders reference column', () => { + const result = strip(babar(pts, { direction: 'horizontal', refY: 40, color: 'ascii', width: 50 })); + expect(result).toContain('|'); + }); +}); From 63fcd99845a9df1158afc8b36e8ec87abe364da6 Mon Sep 17 00:00:00 2001 From: Aurelien LAJOIE Date: Sat, 23 May 2026 22:23:09 +0200 Subject: [PATCH 3/4] Fix horizontal numeric chart label alignment maxLabelW was computed only from minX, so single-digit x values (0, 2) produced narrower labels than double-digit ones (10, 12), shifting bar start positions. Now measured across all generated x labels. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/babar.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/babar.ts b/src/babar.ts index 3167148..60d23b3 100644 --- a/src/babar.ts +++ b/src/babar.ts @@ -255,11 +255,20 @@ function renderHorizontalNumeric( const yFractions = options.yFractions ?? 1; const xFractions = options.xFractions ?? 0; - const maxLabelW = (minX).toFixed(xFractions).length; - const barAreaW = width - maxLabelW - 2 - (effectiveMaxY.toFixed(yFractions).length + 1); - const numBkts = Math.max(1, Math.min(stats.uniqueX, height)); + // Build X labels first so we can measure the longest one + const labels: string[] = []; + for (let u = 0; u < numBkts; u++) { + labels.push((minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions)); + } + const maxLabelW = Math.max(...labels.map(l => l.length)); + const valLabelW = Math.max( + effectiveMinY.toFixed(yFractions).length, + effectiveMaxY.toFixed(yFractions).length, + ); + const barAreaW = Math.max(1, width - maxLabelW - 2 - valLabelW - 1); + const { bkt, min, max, diff } = bucketize( points, numBkts, minX, diffX, options.minY, options.maxY, barAreaW + 1, aggregation, yScale, ); @@ -276,12 +285,6 @@ function renderHorizontalNumeric( } } - // X-axis labels (one per bucket row) - const labels: string[] = []; - for (let u = 0; u < numBkts; u++) { - labels.push((minX + u * (diffX || 0) / Math.max(1, numBkts - 1)).toFixed(xFractions)); - } - // Value labels per bucket const valLabels = bkt.map(v => { const raw = yScale === 'log' From 1877b4bb2daa4f62f1e7b20981822b40c453780b Mon Sep 17 00:00:00 2001 From: Aurelien LAJOIE Date: Sat, 23 May 2026 22:59:25 +0200 Subject: [PATCH 4/4] Replace chalk with inline ANSI helper, bundle with esbuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add src/ansi.ts: 15-line ANSI colorize/bold replacing chalk entirely - Switch build from tsc (JS emit) to esbuild --bundle --minify for lib/babar.js - tsc now only emits .d.ts declarations (emitDeclarationOnly: true) - Remove chalk from dependencies (bundled → zero runtime deps) - Bundle size: 31KB → 9.3KB (same as original CoffeeScript output) - lib/ output: single babar.js + .d.ts files only (no .js.map, no sub-modules) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- package-lock.json | 796 +++++++++++++++++++++++++++++++++++----------- package.json | 12 +- src/ansi.ts | 22 ++ src/babar.ts | 15 +- src/draw.ts | 8 +- tsconfig.json | 5 +- 6 files changed, 641 insertions(+), 217 deletions(-) create mode 100644 src/ansi.ts diff --git a/package-lock.json b/package-lock.json index d56497e..2b08197 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,20 +8,18 @@ "name": "babar", "version": "0.3.0", "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, "devDependencies": { "@types/node": "^20.0.0", + "esbuild": "^0.28.0", "rimraf": "^3.0.2", "typescript": "^5.4.5", "vitest": "^1.6.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -32,13 +30,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -49,13 +47,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -66,13 +64,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -83,13 +81,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -100,13 +98,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -117,13 +115,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -134,13 +132,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -151,13 +149,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -168,13 +166,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -185,13 +183,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -202,13 +200,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -219,13 +217,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -236,13 +234,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -253,13 +251,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -270,13 +268,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -287,13 +285,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -304,13 +302,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -321,13 +336,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -338,13 +370,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -355,13 +404,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -372,13 +421,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -389,13 +438,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -406,7 +455,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jest/schemas": { @@ -903,21 +952,6 @@ "node": ">=0.4.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -975,22 +1009,6 @@ "node": ">=4" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -1004,24 +1022,6 @@ "node": "*" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1093,9 +1093,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1103,32 +1103,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/estree-walker": { @@ -1232,15 +1235,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -1773,18 +1767,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1933,6 +1915,436 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/vitest": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", diff --git a/package.json b/package.json index 86c9902..c1df823 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "types": "lib/babar.d.ts", "type": "commonjs", "scripts": { - "build": "tsc", + "build": "npm run build:bundle && npm run build:types", + "build:bundle": "esbuild src/babar.ts --bundle --platform=node --format=cjs --minify --footer:js=\"module.exports=module.exports.default??module.exports;\" --outfile=lib/babar.js", + "build:types": "tsc", "clean": "rimraf lib coverage", "preversion": "npm run clean", "version": "npm run build", @@ -32,13 +34,11 @@ "bugs": { "url": "https://github.com/stephan83/babar/issues" }, - "dependencies": { - "chalk": "^4.1.2" - }, "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.28.0", "rimraf": "^3.0.2", "typescript": "^5.4.5", - "vitest": "^1.6.0", - "@types/node": "^20.0.0" + "vitest": "^1.6.0" } } diff --git a/src/ansi.ts b/src/ansi.ts new file mode 100644 index 0000000..2d04c60 --- /dev/null +++ b/src/ansi.ts @@ -0,0 +1,22 @@ +import type { Color } from './types.js'; + +// Standard ANSI foreground color codes for the colors we support +const FG: Record = { + black: 30, red: 31, green: 32, yellow: 33, + blue: 34, magenta: 35, cyan: 36, white: 37, + grey: 90, gray: 90, +}; + +function esc(code: number, str: string): string { + return `\x1b[${code}m${str}\x1b[0m`; +} + +export function colorize(str: string, color: Color): string { + if (color === 'ascii') return str; + const code = FG[color]; + return code !== undefined ? esc(code, str) : str; +} + +export function bold(str: string): string { + return esc(1, str); +} diff --git a/src/babar.ts b/src/babar.ts index 60d23b3..32e54ac 100644 --- a/src/babar.ts +++ b/src/babar.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk'; +import { bold } from './ansi.js'; import { validatePoints, validateOptions } from './validate.js'; import { computeStats, bucketize, bucketizeCategorical } from './buckets.js'; import { drawChart, drawHorizontalChart } from './draw.js'; @@ -121,7 +121,7 @@ export default function babar(points: AnyPoint[], options: Options = {}): string let out = ''; if (caption !== undefined) { out += repeat(lblYW, ' '); - out += color === 'ascii' ? caption : chalk.bold(caption); + out += color === 'ascii' ? caption : bold(caption); out += '\n'; } @@ -180,7 +180,7 @@ function renderCategorical( let out = ''; if (caption !== undefined) { - out += color === 'ascii' ? caption : chalk.bold(caption); + out += color === 'ascii' ? caption : bold(caption); out += '\n'; } out += drawHorizontalChart(barWidths, labels, maxLabelW, barAreaW, color, grid, valLabels); @@ -211,7 +211,7 @@ function renderCategorical( let out = ''; if (caption !== undefined) { out += repeat(lblYW, ' '); - out += color === 'ascii' ? caption : chalk.bold(caption); + out += color === 'ascii' ? caption : bold(caption); out += '\n'; } @@ -295,15 +295,10 @@ function renderHorizontalNumeric( let out = ''; if (caption !== undefined) { - out += color === 'ascii' ? caption : chalk.bold(caption); + out += color === 'ascii' ? caption : bold(caption); out += '\n'; } out += drawHorizontalChart(bkt, labels, maxLabelW, barAreaW, color, grid, valLabels, refCol); return out; } -// Allow `require('babar')` to work directly without `.default` -if (typeof module !== 'undefined') { - module.exports = babar; - module.exports.default = babar; -} diff --git a/src/draw.ts b/src/draw.ts index fee640d..2ce180e 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk'; +import { colorize } from './ansi.js'; import type { Color } from './types.js'; // 8-level vertical block characters: index 0 = empty, 8 = full block @@ -12,12 +12,6 @@ function repeat(n: number, ch: string): string { return ch.repeat(n); } -function colorize(str: string, color: Color): string { - if (color === 'ascii') return str; - const fn = (chalk as unknown as Record string>)[color]; - return fn ? fn(str) : str; -} - function blockChar(fraction: number): string { const idx = Math.round(fraction * 8); return BLOCKS[Math.min(8, Math.max(0, idx))]; diff --git a/tsconfig.json b/tsconfig.json index 497c387..9011cc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,9 @@ "rootDir": "src", "strict": true, "declaration": true, - "declarationMap": true, - "sourceMap": true, + "declarationMap": false, + "sourceMap": false, + "emitDeclarationOnly": true, "esModuleInterop": true, "skipLibCheck": true },