diff --git a/ggsql-vscode/.gitignore b/ggsql-vscode/.gitignore
index 1fcb1529..200e9bb8 100644
--- a/ggsql-vscode/.gitignore
+++ b/ggsql-vscode/.gitignore
@@ -1 +1,2 @@
out
+out-test
diff --git a/ggsql-vscode/.vscodeignore b/ggsql-vscode/.vscodeignore
index 4a7a2ca4..84fdc832 100644
--- a/ggsql-vscode/.vscodeignore
+++ b/ggsql-vscode/.vscodeignore
@@ -4,14 +4,18 @@
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
+**/tsconfig.test.json
**/.eslintrc.json
+eslint.config.mjs
**/*.map
**/*.ts
!**/*.d.ts
node_modules/**
.editorconfig
src/**
+test/**
esbuild.js
out/**/*.map
+out-test/**
package-lock.json
*.vsix
diff --git a/ggsql-vscode/README.md b/ggsql-vscode/README.md
index 6f5cca2d..85bb7748 100644
--- a/ggsql-vscode/README.md
+++ b/ggsql-vscode/README.md
@@ -7,6 +7,7 @@
- Complete syntax highlighting for ggsql queries.
- `.ggsql` file extension support.
- Language runtime integration for [Positron IDE](https://positron.posit.co).
+- Render Plot toolbar command with a Markdown-preview-style chart tab.
## Example
diff --git a/ggsql-vscode/esbuild.js b/ggsql-vscode/esbuild.js
index c49e0642..276bf8eb 100644
--- a/ggsql-vscode/esbuild.js
+++ b/ggsql-vscode/esbuild.js
@@ -4,7 +4,7 @@ const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
async function main() {
- const ctx = await esbuild.context({
+ const extensionCtx = await esbuild.context({
entryPoints: ['src/extension.ts'],
bundle: true,
format: 'cjs',
@@ -17,11 +17,26 @@ async function main() {
logLevel: 'info',
});
+ const previewCtx = await esbuild.context({
+ entryPoints: ['src/previewWebview.ts'],
+ bundle: true,
+ format: 'iife',
+ minify: production,
+ sourcemap: !production,
+ sourcesContent: false,
+ platform: 'browser',
+ outfile: 'out/previewWebview.js',
+ logLevel: 'info',
+ });
+
if (watch) {
- await ctx.watch();
+ await extensionCtx.watch();
+ await previewCtx.watch();
} else {
- await ctx.rebuild();
- await ctx.dispose();
+ await extensionCtx.rebuild();
+ await previewCtx.rebuild();
+ await extensionCtx.dispose();
+ await previewCtx.dispose();
}
}
diff --git a/ggsql-vscode/package-lock.json b/ggsql-vscode/package-lock.json
index 3bdc77ab..7d541429 100644
--- a/ggsql-vscode/package-lock.json
+++ b/ggsql-vscode/package-lock.json
@@ -20,7 +20,10 @@
"esbuild": "^0.27.4",
"eslint": "^10.0.3",
"npm-run-all": "^4.1.5",
- "typescript": "^5.0.0"
+ "typescript": "^5.0.0",
+ "vega": "^6.2.0",
+ "vega-embed": "^7.1.0",
+ "vega-lite": "^6.4.2"
},
"engines": {
"vscode": "^1.75.0"
@@ -641,6 +644,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -938,6 +948,19 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -1114,6 +1137,21 @@
"node": ">=0.8.0"
}
},
+ "node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1131,6 +1169,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1153,6 +1201,261 @@
"node": ">= 8"
}
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo-projection": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
+ "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "d3-array": "1 - 3",
+ "d3-geo": "1.12.0 - 3"
+ },
+ "bin": {
+ "geo2svg": "bin/geo2svg.js",
+ "geograticule": "bin/geograticule.js",
+ "geoproject": "bin/geoproject.js",
+ "geoquantize": "bin/geoquantize.js",
+ "geostitch": "bin/geostitch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -1268,6 +1571,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1283,6 +1596,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -1471,6 +1791,16 @@
"@esbuild/win32-x64": "0.27.4"
}
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -1679,6 +2009,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-json-patch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz",
+ "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -1829,6 +2166,29 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
+ "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2037,6 +2397,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -2072,6 +2445,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -2503,6 +2886,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-stringify-pretty-compact": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3070,6 +3460,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "dev": true,
+ "license": "Unlicense"
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -3125,6 +3529,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -3349,6 +3760,24 @@
"node": ">= 0.4"
}
},
+ "node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/string.prototype.padend": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
@@ -3427,6 +3856,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -3486,6 +3931,28 @@
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
"license": "MIT"
},
+ "node_modules/topojson-client": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
+ "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "commander": "2"
+ },
+ "bin": {
+ "topo2geo": "bin/topo2geo",
+ "topomerge": "bin/topomerge",
+ "topoquantize": "bin/topoquantize"
+ }
+ },
+ "node_modules/topojson-client/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -3499,6 +3966,13 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3649,6 +4123,510 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "node_modules/vega": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz",
+ "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-crossfilter": "~5.1.0",
+ "vega-dataflow": "~6.1.0",
+ "vega-encode": "~5.1.0",
+ "vega-event-selector": "~4.0.0",
+ "vega-expression": "~6.1.0",
+ "vega-force": "~5.1.0",
+ "vega-format": "~2.1.0",
+ "vega-functions": "~6.1.0",
+ "vega-geo": "~5.1.0",
+ "vega-hierarchy": "~5.1.0",
+ "vega-label": "~2.1.0",
+ "vega-loader": "~5.1.0",
+ "vega-parser": "~7.1.0",
+ "vega-projection": "~2.1.0",
+ "vega-regression": "~2.1.0",
+ "vega-runtime": "~7.1.0",
+ "vega-scale": "~8.1.0",
+ "vega-scenegraph": "~5.1.0",
+ "vega-statistics": "~2.0.0",
+ "vega-time": "~3.1.0",
+ "vega-transforms": "~5.1.0",
+ "vega-typings": "~2.1.0",
+ "vega-util": "~2.1.0",
+ "vega-view": "~6.1.0",
+ "vega-view-transforms": "~5.1.0",
+ "vega-voronoi": "~5.1.0",
+ "vega-wordcloud": "~5.1.0"
+ },
+ "funding": {
+ "url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
+ }
+ },
+ "node_modules/vega-canvas": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz",
+ "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/vega-crossfilter": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz",
+ "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "vega-dataflow": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-dataflow": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz",
+ "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-format": "^2.1.0",
+ "vega-loader": "^5.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-embed": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-7.1.0.tgz",
+ "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "fast-json-patch": "^3.1.1",
+ "json-stringify-pretty-compact": "^4.0.0",
+ "semver": "^7.7.2",
+ "tslib": "^2.8.1",
+ "vega-interpreter": "^2.0.0",
+ "vega-schema-url-parser": "^3.0.2",
+ "vega-themes": "3.0.0",
+ "vega-tooltip": "1.0.0"
+ },
+ "funding": {
+ "url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
+ },
+ "peerDependencies": {
+ "vega": "*",
+ "vega-lite": "*"
+ }
+ },
+ "node_modules/vega-encode": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz",
+ "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-interpolate": "^3.0.1",
+ "vega-dataflow": "^6.1.0",
+ "vega-scale": "^8.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-event-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz",
+ "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/vega-expression": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz",
+ "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/estree": "^1.0.8",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-force": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz",
+ "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-force": "^3.0.0",
+ "vega-dataflow": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-format": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz",
+ "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-format": "^3.1.0",
+ "d3-time-format": "^4.1.0",
+ "vega-time": "^3.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-functions": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.1.tgz",
+ "integrity": "sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-color": "^3.1.0",
+ "d3-geo": "^3.1.1",
+ "vega-dataflow": "^6.1.0",
+ "vega-expression": "^6.1.0",
+ "vega-scale": "^8.1.0",
+ "vega-scenegraph": "^5.1.0",
+ "vega-selections": "^6.1.0",
+ "vega-statistics": "^2.0.0",
+ "vega-time": "^3.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-geo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz",
+ "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-color": "^3.1.0",
+ "d3-geo": "^3.1.1",
+ "vega-canvas": "^2.0.0",
+ "vega-dataflow": "^6.1.0",
+ "vega-projection": "^2.1.0",
+ "vega-statistics": "^2.0.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-hierarchy": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz",
+ "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-hierarchy": "^3.1.2",
+ "vega-dataflow": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-interpreter": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-2.2.1.tgz",
+ "integrity": "sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-label": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz",
+ "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-canvas": "^2.0.0",
+ "vega-dataflow": "^6.1.0",
+ "vega-scenegraph": "^5.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-lite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.2.tgz",
+ "integrity": "sha512-Mv2PaRIpijz256LM0NdOJd9Md8cqyrXina54xW6Qp865YfY502zlXGUst+W/XznVwISGfatt0yLZuDqCUbBDuw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "json-stringify-pretty-compact": "~4.0.0",
+ "tslib": "~2.8.1",
+ "vega-event-selector": "~4.0.0",
+ "vega-expression": "~6.1.0",
+ "vega-util": "~2.1.0",
+ "yargs": "~18.0.0"
+ },
+ "bin": {
+ "vl2pdf": "bin/vl2pdf",
+ "vl2png": "bin/vl2png",
+ "vl2svg": "bin/vl2svg",
+ "vl2vg": "bin/vl2vg"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
+ },
+ "peerDependencies": {
+ "vega": "^6.0.0"
+ }
+ },
+ "node_modules/vega-loader": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz",
+ "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-dsv": "^3.0.1",
+ "topojson-client": "^3.1.0",
+ "vega-format": "^2.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-parser": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz",
+ "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-dataflow": "^6.1.0",
+ "vega-event-selector": "^4.0.0",
+ "vega-functions": "^6.1.0",
+ "vega-scale": "^8.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-projection": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz",
+ "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-geo": "^3.1.1",
+ "d3-geo-projection": "^4.0.0",
+ "vega-scale": "^8.1.0"
+ }
+ },
+ "node_modules/vega-regression": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz",
+ "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "vega-dataflow": "^6.1.0",
+ "vega-statistics": "^2.0.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-runtime": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz",
+ "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-dataflow": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-scale": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz",
+ "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-scale-chromatic": "^3.1.0",
+ "vega-time": "^3.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-scenegraph": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz",
+ "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "^3.1.0",
+ "d3-shape": "^3.2.0",
+ "vega-canvas": "^2.0.0",
+ "vega-loader": "^5.1.0",
+ "vega-scale": "^8.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-schema-url-parser": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz",
+ "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/vega-selections": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.2.tgz",
+ "integrity": "sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "3.2.4",
+ "vega-expression": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-statistics": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz",
+ "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4"
+ }
+ },
+ "node_modules/vega-themes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-3.0.0.tgz",
+ "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "funding": {
+ "url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
+ },
+ "peerDependencies": {
+ "vega": "*",
+ "vega-lite": "*"
+ }
+ },
+ "node_modules/vega-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz",
+ "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-time": "^3.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-tooltip": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-1.0.0.tgz",
+ "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-util": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://app.hubspot.com/payments/GyPC972GD9Rt"
+ }
+ },
+ "node_modules/vega-transforms": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz",
+ "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "vega-dataflow": "^6.1.0",
+ "vega-statistics": "^2.0.0",
+ "vega-time": "^3.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-typings": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz",
+ "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/geojson": "7946.0.16",
+ "vega-event-selector": "^4.0.0",
+ "vega-expression": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-util": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz",
+ "integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/vega-view": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz",
+ "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^3.2.4",
+ "d3-timer": "^3.0.1",
+ "vega-dataflow": "^6.1.0",
+ "vega-format": "^2.1.0",
+ "vega-functions": "^6.1.0",
+ "vega-runtime": "^7.1.0",
+ "vega-scenegraph": "^5.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-view-transforms": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz",
+ "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-dataflow": "^6.1.0",
+ "vega-scenegraph": "^5.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-voronoi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz",
+ "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-delaunay": "^6.0.4",
+ "vega-dataflow": "^6.1.0",
+ "vega-util": "^2.1.0"
+ }
+ },
+ "node_modules/vega-wordcloud": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz",
+ "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "vega-canvas": "^2.0.0",
+ "vega-dataflow": "^6.1.0",
+ "vega-scale": "^8.1.0",
+ "vega-statistics": "^2.0.0",
+ "vega-util": "^2.1.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3764,6 +4742,75 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/ggsql-vscode/package.json b/ggsql-vscode/package.json
index 4ad09fc8..1f6141ee 100644
--- a/ggsql-vscode/package.json
+++ b/ggsql-vscode/package.json
@@ -62,6 +62,15 @@
"title": "Source Current File",
"icon": "$(play)"
},
+ {
+ "command": "ggsql.renderPlot",
+ "category": "ggsql",
+ "title": "Render Plot",
+ "icon": {
+ "light": "./resources/render-plot-light.svg",
+ "dark": "./resources/render-plot-dark.svg"
+ }
+ },
{
"command": "ggsql.runQuery",
"category": "ggsql",
@@ -103,6 +112,13 @@
}
],
"menus": {
+ "editor/title": [
+ {
+ "command": "ggsql.renderPlot",
+ "group": "navigation@-1",
+ "when": "resourceLangId == ggsql && !isInDiffEditor"
+ }
+ ],
"editor/title/run": [
{
"command": "ggsql.sourceCurrentFile",
@@ -123,6 +139,16 @@
"type": "string",
"default": "",
"description": "Path to the ggsql-jupyter executable. If empty, uses 'ggsql-jupyter' from PATH."
+ },
+ "ggsql.cliPath": {
+ "type": "string",
+ "default": "",
+ "description": "Path to the ggsql CLI executable used by Render Plot. If empty, uses 'ggsql' from PATH."
+ },
+ "ggsql.readerUri": {
+ "type": "string",
+ "default": "duckdb://memory",
+ "description": "Reader connection URI used by Render Plot, such as duckdb://memory or duckdb:///path/to/file.db."
}
}
}
@@ -130,6 +156,7 @@
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && node esbuild.js",
+ "test": "tsc --project tsconfig.test.json && node --test out-test/test/*.test.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
@@ -149,7 +176,10 @@
"esbuild": "^0.27.4",
"eslint": "^10.0.3",
"npm-run-all": "^4.1.5",
- "typescript": "^5.0.0"
+ "typescript": "^5.0.0",
+ "vega": "^6.2.0",
+ "vega-embed": "^7.1.0",
+ "vega-lite": "^6.4.2"
},
"overrides": {
"uri-js": "npm:uri-js-replace"
diff --git a/ggsql-vscode/resources/render-plot-dark.svg b/ggsql-vscode/resources/render-plot-dark.svg
new file mode 100644
index 00000000..803979b1
--- /dev/null
+++ b/ggsql-vscode/resources/render-plot-dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/ggsql-vscode/resources/render-plot-light.svg b/ggsql-vscode/resources/render-plot-light.svg
new file mode 100644
index 00000000..b59d9622
--- /dev/null
+++ b/ggsql-vscode/resources/render-plot-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/ggsql-vscode/src/extension.ts b/ggsql-vscode/src/extension.ts
index 75e27d01..47776a4a 100644
--- a/ggsql-vscode/src/extension.ts
+++ b/ggsql-vscode/src/extension.ts
@@ -6,6 +6,10 @@
*/
import * as vscode from 'vscode';
+import * as cp from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import { promisify } from 'util';
import { tryAcquirePositronApi } from '@posit-dev/positron';
import { GgsqlRuntimeManager } from './manager';
import { createConnectionDrivers } from './connections';
@@ -13,6 +17,16 @@ import { GgsqlCodeLensProvider, registerCellCommands } from './codelens';
import { activateDecorations } from './decorations';
import { activateContextKeys } from './context';
import { parseCells } from './cellParser';
+import {
+ buildPreviewHtml,
+ buildRenderArgs,
+ getGgsqlCliPath,
+ getReaderUri,
+} from './preview';
+
+const execFile = promisify(cp.execFile);
+const INSTALL_URL = 'https://ggsql.org/get_started/installation.html';
+const previewPanels = new Map();
// Output channel for logging
const outputChannel = vscode.window.createOutputChannel('ggsql');
@@ -32,6 +46,14 @@ export function activate(context: vscode.ExtensionContext): void {
// Try to acquire the Positron API
const positronApi = tryAcquirePositronApi();
+ context.subscriptions.push(
+ vscode.commands.registerCommand('ggsql.renderPlot', async () => {
+ await renderPlot(context);
+ })
+ );
+
+ void checkCliAvailability();
+
if (!positronApi) {
// Running in VS Code (not Positron) - syntax highlighting still works
// but we don't register the language runtime
@@ -99,3 +121,234 @@ export function activate(context: vscode.ExtensionContext): void {
export function deactivate(): void {
// Nothing to clean up
}
+
+async function renderPlot(context: vscode.ExtensionContext): Promise {
+ outputChannel.show(true);
+ log('Render Plot clicked');
+
+ const editor = vscode.window.activeTextEditor;
+ if (!editor || editor.document.languageId !== 'ggsql') {
+ log('Render Plot skipped: no active ggsql editor');
+ return;
+ }
+
+ const query = getPreviewQuery(editor);
+ if (query.trim().length === 0) {
+ log('Render Plot skipped: query is empty');
+ return;
+ }
+
+ log(`Document: ${editor.document.uri.toString()}`);
+ log(`Query source: ${editor.selection.isEmpty ? 'current file' : 'selection'}`);
+ log(`Query size: ${query.length} chars`);
+
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: 'Rendering ggsql plot',
+ cancellable: false,
+ },
+ async () => {
+ const config = vscode.workspace.getConfiguration('ggsql');
+ const cliPath = getGgsqlCliPath(config);
+ const readerUri = getReaderUri(config);
+ const previewDir = path.join(context.globalStorageUri.fsPath, 'preview');
+ const runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
+ const queryPath = path.join(previewDir, `${runId}.ggsql`);
+ const outputPath = path.join(previewDir, `${runId}.vl.json`);
+ const cwd = getDocumentWorkingDirectory(editor.document);
+
+ log(`CLI path: ${cliPath}`);
+ log(`Reader URI: ${readerUri}`);
+ log(`Working directory: ${cwd ?? '(none)'}`);
+ log(`Preview directory: ${previewDir}`);
+ log(`Query temp file: ${queryPath}`);
+ log(`Vega-Lite output file: ${outputPath}`);
+
+ await fs.promises.mkdir(previewDir, { recursive: true });
+ await fs.promises.writeFile(queryPath, query, 'utf8');
+ log('Wrote query temp file');
+
+ const args = buildRenderArgs(queryPath, outputPath, readerUri);
+ log(`Running command: ${formatCommand(cliPath, args)}`);
+ const startedAt = Date.now();
+
+ try {
+ const result = await execFile(cliPath, args, {
+ cwd,
+ timeout: 120_000,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+ log(`ggsql CLI exited in ${Date.now() - startedAt}ms`);
+ if (result.stdout.trim()) {
+ log(`stdout:\n${result.stdout.trim()}`);
+ }
+ if (result.stderr.trim()) {
+ log(`stderr:\n${result.stderr.trim()}`);
+ }
+ } catch (error) {
+ log(`ggsql CLI failed after ${Date.now() - startedAt}ms`);
+ logRenderError(error);
+ vscode.window.showErrorMessage(renderErrorMessage(error, cliPath));
+ return;
+ }
+
+ let specJson: string;
+ try {
+ specJson = await fs.promises.readFile(outputPath, 'utf8');
+ } catch (error) {
+ logRenderError(error);
+ vscode.window.showErrorMessage(`ggsql did not write a Vega-Lite spec: ${errorMessage(error)}`);
+ return;
+ }
+
+ log(`Read Vega-Lite spec (${Buffer.byteLength(specJson, 'utf8')} bytes)`);
+ showPreview(context, editor.document, specJson);
+ }
+ );
+}
+
+function getPreviewQuery(editor: vscode.TextEditor): string {
+ if (!editor.selection.isEmpty) {
+ return editor.document.getText(editor.selection);
+ }
+ return editor.document.getText();
+}
+
+function getDocumentWorkingDirectory(document: vscode.TextDocument): string | undefined {
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
+ if (workspaceFolder) {
+ return workspaceFolder.uri.fsPath;
+ }
+
+ if (document.uri.scheme === 'file') {
+ return path.dirname(document.uri.fsPath);
+ }
+
+ return undefined;
+}
+
+function showPreview(context: vscode.ExtensionContext, document: vscode.TextDocument, specJson: string): void {
+ const key = document.uri.toString();
+ const title = `Preview ${path.basename(document.fileName || 'plot')}`;
+ const existingPanel = previewPanels.get(key);
+ if (existingPanel) {
+ log(`Preview panel reused: ${title}`);
+ existingPanel.reveal(vscode.ViewColumn.Beside);
+ updatePreviewHtml(context, existingPanel, title, specJson);
+ return;
+ }
+
+ log(`Preview panel created: ${title}`);
+ const panel = vscode.window.createWebviewPanel(
+ 'ggsql.plotPreview',
+ title,
+ vscode.ViewColumn.Beside,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ }
+ );
+ previewPanels.set(key, panel);
+ panel.onDidDispose(() => {
+ previewPanels.delete(key);
+ });
+ updatePreviewHtml(context, panel, title, specJson);
+}
+
+function updatePreviewHtml(
+ context: vscode.ExtensionContext,
+ panel: vscode.WebviewPanel,
+ title: string,
+ specJson: string
+): void {
+ try {
+ const rendererScriptUri = panel.webview.asWebviewUri(
+ vscode.Uri.joinPath(context.extensionUri, 'out', 'previewWebview.js')
+ );
+ panel.webview.html = buildPreviewHtml(specJson, {
+ cspSource: panel.webview.cspSource,
+ rendererScriptUri: rendererScriptUri.toString(),
+ title,
+ });
+ log(`Preview HTML updated: ${title}`);
+ } catch (error) {
+ panel.dispose();
+ logRenderError(error);
+ vscode.window.showErrorMessage(`Failed to open ggsql preview: ${errorMessage(error)}`);
+ }
+}
+
+function renderErrorMessage(error: unknown, cliPath: string): string {
+ const err = error as NodeJS.ErrnoException & { stderr?: string };
+ if (err.code === 'ENOENT') {
+ void showMissingCliMessage(cliPath);
+ return `Could not find ggsql CLI at '${cliPath}'. Install ggsql, set ggsql.cliPath, or add ggsql to PATH.`;
+ }
+
+ const stderr = typeof err.stderr === 'string' ? err.stderr.trim() : '';
+ return stderr || errorMessage(error);
+}
+
+function logRenderError(error: unknown): void {
+ log(errorMessage(error));
+ const err = error as { stderr?: string; stdout?: string };
+ if (typeof err.stdout === 'string' && err.stdout.trim()) {
+ log(err.stdout.trim());
+ }
+ if (typeof err.stderr === 'string' && err.stderr.trim()) {
+ log(err.stderr.trim());
+ }
+}
+
+function errorMessage(error: unknown): string {
+ return error instanceof Error ? error.message : String(error);
+}
+
+function formatCommand(command: string, args: string[]): string {
+ return [command, ...args].map(formatCommandArg).join(' ');
+}
+
+function formatCommandArg(arg: string): string {
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(arg)) {
+ return arg;
+ }
+
+ return JSON.stringify(arg);
+}
+
+async function checkCliAvailability(): Promise {
+ const config = vscode.workspace.getConfiguration('ggsql');
+ const cliPath = getGgsqlCliPath(config);
+ try {
+ const result = await execFile(cliPath, ['--version'], {
+ timeout: 5000,
+ maxBuffer: 1024 * 1024,
+ });
+ const version = result.stdout.trim() || result.stderr.trim();
+ if (version) {
+ log(`Found ggsql CLI: ${version}`);
+ }
+ } catch (error) {
+ const err = error as NodeJS.ErrnoException;
+ if (err.code === 'ENOENT') {
+ await showMissingCliMessage(cliPath);
+ } else {
+ log(`Could not verify ggsql CLI '${cliPath}': ${errorMessage(error)}`);
+ }
+ }
+}
+
+async function showMissingCliMessage(cliPath: string): Promise {
+ const selection = await vscode.window.showWarningMessage(
+ `ggsql CLI not found at '${cliPath}'. Render Plot needs the ggsql CLI.`,
+ 'Install ggsql',
+ 'Set CLI Path'
+ );
+
+ if (selection === 'Install ggsql') {
+ await vscode.env.openExternal(vscode.Uri.parse(INSTALL_URL));
+ } else if (selection === 'Set CLI Path') {
+ await vscode.commands.executeCommand('workbench.action.openSettings', 'ggsql.cliPath');
+ }
+}
diff --git a/ggsql-vscode/src/preview.ts b/ggsql-vscode/src/preview.ts
new file mode 100644
index 00000000..2045f373
--- /dev/null
+++ b/ggsql-vscode/src/preview.ts
@@ -0,0 +1,169 @@
+export const DEFAULT_READER_URI = 'duckdb://memory';
+
+interface ConfigReader {
+ get(name: string, defaultValue: T): T;
+}
+
+interface PreviewHtmlOptions {
+ cspSource: string;
+ rendererScriptUri: string;
+ nonce?: string;
+ title?: string;
+}
+
+export function getGgsqlCliPath(config: ConfigReader): string {
+ const configuredPath = config.get('cliPath', '').trim();
+ return configuredPath || 'ggsql';
+}
+
+export function getReaderUri(config: ConfigReader): string {
+ const readerUri = config.get('readerUri', DEFAULT_READER_URI).trim();
+ return readerUri || DEFAULT_READER_URI;
+}
+
+export function buildRenderArgs(queryFile: string, outputFile: string, readerUri: string): string[] {
+ return [
+ 'run',
+ queryFile,
+ '--reader',
+ readerUri,
+ '--writer',
+ 'vegalite',
+ '--output',
+ outputFile,
+ ];
+}
+
+export function buildPreviewHtml(specJson: string, options: PreviewHtmlOptions): string {
+ const parsedSpec = JSON.parse(specJson) as unknown;
+ const title = options.title ?? 'ggsql Plot';
+ const nonce = options.nonce ?? createNonce();
+ const encodedSpec = escapeForScript(JSON.stringify(parsedSpec));
+ const safeTitle = escapeHtml(title);
+
+ return `
+
+
+
+
+
+ ${safeTitle}
+
+
+
+
+
+
+
+`;
+}
+
+function createNonce(): string {
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let nonce = '';
+ for (let i = 0; i < 32; i++) {
+ nonce += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+ }
+ return nonce;
+}
+
+function escapeForScript(value: string): string {
+ return value
+ .replace(//g, '\\u003e')
+ .replace(/&/g, '\\u0026')
+ .replace(/\u2028/g, '\\u2028')
+ .replace(/\u2029/g, '\\u2029');
+}
+
+function escapeHtml(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
diff --git a/ggsql-vscode/src/previewWebview.ts b/ggsql-vscode/src/previewWebview.ts
new file mode 100644
index 00000000..263303c4
--- /dev/null
+++ b/ggsql-vscode/src/previewWebview.ts
@@ -0,0 +1,88 @@
+type VegaLiteSpec = Record & {
+ autosize?: unknown;
+ height?: unknown;
+ width?: unknown;
+};
+
+type EmbedPlot = (
+ selector: string,
+ spec: VegaLiteSpec,
+ options: { actions: boolean; ast: boolean; renderer: 'svg' }
+) => Promise;
+
+async function renderPlot(): Promise {
+ const specElement = document.getElementById('ggsql-spec');
+ if (!specElement?.textContent) {
+ throw new Error('Missing Vega-Lite spec.');
+ }
+
+ const { default: embed } = await import('vega-embed');
+ const spec = sizeSpec(JSON.parse(specElement.textContent) as VegaLiteSpec);
+ const embedPlot = embed as unknown as EmbedPlot;
+ resetRenderTarget();
+ await embedPlot('#vis', spec, {
+ actions: true,
+ ast: true,
+ renderer: 'svg',
+ });
+}
+
+function sizeSpec(spec: VegaLiteSpec): VegaLiteSpec {
+ const { width, height } = getChartSize();
+ const resolvedWidth = spec.width ?? width;
+ const resolvedHeight = spec.height ?? height;
+
+ return {
+ ...spec,
+ autosize: spec.autosize ?? { type: 'fit', contains: 'padding', resize: true },
+ width: resolveDimension(resolvedWidth, width),
+ height: resolveDimension(resolvedHeight, height),
+ };
+}
+
+function resolveDimension(dimension: unknown, fallback: number): unknown {
+ return dimension === 'container' ? fallback : dimension;
+}
+
+function getChartSize(): { width: number; height: number } {
+ const chartView = document.getElementById('chart-view');
+ const rect = chartView?.getBoundingClientRect();
+ const width = Math.max(320, Math.floor((rect?.width ?? window.innerWidth) - 32));
+ const height = Math.max(280, Math.floor((rect?.height ?? window.innerHeight) - 32));
+
+ return { width, height };
+}
+
+function resetRenderTarget(): void {
+ const vis = document.getElementById('vis');
+ if (vis) {
+ vis.replaceChildren();
+ }
+
+ const errorEl = document.getElementById('render-error');
+ if (errorEl) {
+ errorEl.style.display = 'none';
+ errorEl.textContent = '';
+ }
+}
+
+function showError(error: unknown): void {
+ const errorEl = document.getElementById('render-error');
+ if (!errorEl) {
+ return;
+ }
+ errorEl.style.display = 'block';
+ errorEl.textContent = error instanceof Error ? error.message : String(error);
+}
+
+let resizeHandle: ReturnType | undefined;
+window.addEventListener('resize', () => {
+ if (resizeHandle) {
+ clearTimeout(resizeHandle);
+ }
+ resizeHandle = setTimeout(() => {
+ renderPlot().catch(showError);
+ }, 150);
+});
+
+renderPlot().catch(showError);
diff --git a/ggsql-vscode/test/preview.test.ts b/ggsql-vscode/test/preview.test.ts
new file mode 100644
index 00000000..fbd24a44
--- /dev/null
+++ b/ggsql-vscode/test/preview.test.ts
@@ -0,0 +1,134 @@
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import test from 'node:test';
+import {
+ buildPreviewHtml,
+ buildRenderArgs,
+ getGgsqlCliPath,
+ getReaderUri,
+} from '../src/preview';
+
+test('buildRenderArgs runs ggsql with file input and Vega-Lite output', () => {
+ assert.deepEqual(
+ buildRenderArgs('/tmp/query.ggsql', '/tmp/plot.vl.json', 'duckdb://memory'),
+ [
+ 'run',
+ '/tmp/query.ggsql',
+ '--reader',
+ 'duckdb://memory',
+ '--writer',
+ 'vegalite',
+ '--output',
+ '/tmp/plot.vl.json',
+ ],
+ );
+});
+
+test('preview settings fall back to ggsql binary and in-memory DuckDB', () => {
+ const emptyConfig = {
+ get(_name: string, fallback: T): T {
+ return fallback;
+ },
+ };
+
+ assert.equal(getGgsqlCliPath(emptyConfig), 'ggsql');
+ assert.equal(getReaderUri(emptyConfig), 'duckdb://memory');
+});
+
+test('buildPreviewHtml embeds a Vega-Lite spec without raw script-breaking text', () => {
+ const spec = JSON.stringify({
+ title: '
',
+ data: { values: [{ x: 1, y: 2 }] },
+ mark: 'point',
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'y', type: 'quantitative' },
+ },
+ });
+
+ const html = buildPreviewHtml(spec, {
+ cspSource: 'vscode-resource:',
+ rendererScriptUri: 'vscode-resource:/out/previewWebview.js',
+ nonce: 'test-nonce',
+ title: 'ggsql test plot',
+ });
+
+ assert.match(html, /script-src 'nonce-test-nonce' vscode-resource:/);
+ assert.match(html, /ggsql test plot/);
+ assert(!html.includes('
{
+ const html = buildPreviewHtml('{"mark":"point","data":{"values":[]}}', {
+ cspSource: 'vscode-resource:',
+ rendererScriptUri: 'vscode-resource:/out/previewWebview.js',
+ nonce: 'test-nonce',
+ title: 'ggsql test plot',
+ });
+
+ assert.match(html, /id="chart-view"/);
+ assert(!html.includes('data-mode="code"'));
+ assert(!html.includes('data-mode="both"'));
+ assert(!html.includes('Generated Vega-Lite JSON'));
+});
+
+test('extension uses real SVG icon files for top-level render action', () => {
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+ const renderCommand = packageJson.contributes.commands.find(
+ (command: { command: string }) => command.command === 'ggsql.renderPlot'
+ );
+ const renderMenu = packageJson.contributes.menus['editor/title'].find(
+ (item: { command: string }) => item.command === 'ggsql.renderPlot'
+ );
+
+ assert.deepEqual(renderCommand.icon, {
+ light: './resources/render-plot-light.svg',
+ dark: './resources/render-plot-dark.svg',
+ });
+ assert.equal(fs.existsSync('resources/render-plot-light.svg'), true);
+ assert.equal(fs.existsSync('resources/render-plot-dark.svg'), true);
+ assert.equal(renderMenu.group, 'navigation@-1');
+});
+
+test('webview renderer uses Vega AST interpreter for CSP-safe rendering', () => {
+ const source = fs.readFileSync('src/previewWebview.ts', 'utf8');
+
+ assert.match(source, /ast:\s*true/);
+ assert(!source.includes('unsafe-eval'));
+});
+
+test('webview renderer fits chart to available preview area', () => {
+ const source = fs.readFileSync('src/previewWebview.ts', 'utf8');
+
+ assert.match(source, /getBoundingClientRect/);
+ assert.match(source, /autosize:\s*spec\.autosize\s*\?\?/);
+ assert.match(source, /type:\s*'fit'/);
+ assert.match(source, /resize:\s*true/);
+ assert.match(source, /const resolvedWidth = spec\.width \?\? width/);
+ assert.match(source, /const resolvedHeight = spec\.height \?\? height/);
+ assert.match(source, /width:\s*resolveDimension\(resolvedWidth, width\)/);
+ assert.match(source, /height:\s*resolveDimension\(resolvedHeight, height\)/);
+});
+
+test('webview renderer resolves Vega-Lite container dimensions to preview pixels', () => {
+ const source = fs.readFileSync('src/previewWebview.ts', 'utf8');
+
+ assert.match(source, /resolveDimension/);
+ assert.match(source, /dimension === 'container'/);
+});
+
+test('render command writes detailed diagnostics to the ggsql output channel', () => {
+ const source = fs.readFileSync('src/extension.ts', 'utf8');
+
+ assert.match(source, /outputChannel\.show\(true\)/);
+ assert.match(source, /Render Plot clicked/);
+ assert.match(source, /Working directory:/);
+ assert.match(source, /Query temp file:/);
+ assert.match(source, /Vega-Lite output file:/);
+ assert.match(source, /ggsql CLI exited/);
+ assert.match(source, /Preview panel/);
+});
diff --git a/ggsql-vscode/tsconfig.json b/ggsql-vscode/tsconfig.json
index bbbb17e4..a176b01c 100644
--- a/ggsql-vscode/tsconfig.json
+++ b/ggsql-vscode/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
- "lib": ["ES2022"],
+ "lib": ["ES2022", "DOM"],
"moduleResolution": "Node16",
"outDir": "out",
"sourceMap": true,
diff --git a/ggsql-vscode/tsconfig.test.json b/ggsql-vscode/tsconfig.test.json
new file mode 100644
index 00000000..03577848
--- /dev/null
+++ b/ggsql-vscode/tsconfig.test.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "outDir": "out-test",
+ "rootDir": ".",
+ "sourceMap": false
+ },
+ "include": ["src/preview.ts", "test/**/*.ts"]
+}