├── .gitignore ├── src ├── gnuplot.wasm ├── index.test.js └── index.js ├── package.json ├── Readme.md └── example.svg /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /src/gnuplot.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/gnuplot-wasm/main/src/gnuplot.wasm -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gnuplot-wasm", 3 | "version": "0.1.0", 4 | "description": "gnuplot compiled to WASM", 5 | "keywords": [ 6 | "gnuplot", 7 | "graph", 8 | "plot", 9 | "diagram", 10 | "svg" 11 | ], 12 | "license": "MIT", 13 | "repository": "stereobooster/gnuplot-wasm", 14 | "author": "stereooboster", 15 | "type": "module", 16 | "main": "src/index.js", 17 | "files": [ 18 | "src" 19 | ], 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import gnuplot from "./index.js"; 2 | 3 | const { render, version } = await gnuplot(); 4 | 5 | const script = ` 6 | #!/usr/bin/gnuplot 7 | # 8 | # Plotting filledcurves with different transparencies 9 | # 10 | # AUTHOR: Hagen Wierstorf 11 | # VERSION: gnuplot 4.6 patchlevel 0 12 | 13 | # reset 14 | 15 | # wxt 16 | # set terminal wxt size 350,262 enhanced font 'Verdana,10' persist 17 | # png 18 | # set terminal pngcairo size 350,262 enhanced font 'Verdana,10' 19 | # set output 'different_transparency2.png' 20 | 21 | set border linewidth 1.5 22 | # Axes 23 | set style line 11 lc rgb '#808080' lt 1 24 | set border 3 back ls 11 25 | set tics nomirror out scale 0.75 26 | # Grid 27 | set style line 12 lc rgb'#808080' lt 0 lw 1 28 | set grid back ls 12 29 | 30 | set style fill noborder 31 | set style function filledcurves y1=0 32 | set clip two 33 | 34 | Gauss(x,mu,sigma) = 1./(sigma*sqrt(2*pi)) * exp( -(x-mu)**2 / (2*sigma**2) ) 35 | d1(x) = Gauss(x, 0.5, 0.5) 36 | d2(x) = Gauss(x, 2., 1.) 37 | d3(x) = Gauss(x, -1., 2.) 38 | 39 | set xrange [-5:5] 40 | set yrange [0:1] 41 | 42 | unset colorbox 43 | 44 | set key title "Gaussian Distribution" 45 | set key top left Left reverse samplen 1 46 | 47 | set lmargin 6 48 | plot d1(x) fs transparent solid 0.75 lc rgb "forest-green" title 'µ= 0.5 σ=0.5', \ 49 | d2(x) fs transparent solid 0.50 lc rgb "gold" title 'µ= 2.0 σ=1.0', \ 50 | d3(x) fs transparent solid 0.25 lc rgb "red" title 'µ=-1.0 σ=2.0' 51 | `; 52 | 53 | const { svg } = render(script); 54 | console.log({ svg }); 55 | console.log(version()); 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import gnuplotWrapper from "./gnuplot.js"; 2 | 3 | export default async function init(options = {}) { 4 | let STDOUT = []; 5 | const SCRIPT_FILE = "script.gnuplot"; 6 | const RESULT_FILE = "plot.svg"; 7 | 8 | const instance = await gnuplotWrapper({ 9 | noInitialRun: true, 10 | noFSInit: true, 11 | print: (stdout) => STDOUT.push(stdout), 12 | printErr: (stderr) => STDOUT.push(stderr), 13 | ...options, 14 | }); 15 | 16 | // disable STDIN 17 | instance.FS.init(() => null, null, null); 18 | 19 | function exec(...argv) { 20 | // HACK: gnuplot does not clean up memory when it exits. 21 | // this is OK under normal circumstances because the OS will 22 | // reclaim the memory. But the emscripten runtime does not 23 | // do this, so we create a snapshot of the memory before calling 24 | // main and restore it after calling main. 25 | const mem_snapshot = Uint8Array.from(instance.HEAPU8); 26 | const exitCode = instance.callMain(argv); 27 | instance.HEAPU8.set(mem_snapshot); 28 | 29 | const stdout = STDOUT.join("\n"); 30 | STDOUT = []; 31 | return { 32 | exitCode, 33 | stdout, 34 | }; 35 | } 36 | 37 | function version() { 38 | return exec("--version").stdout; 39 | } 40 | 41 | function render(script, options = {}) { 42 | const term = options.term || "svg"; 43 | const width = options.width || 1000; 44 | const height = options.height || 500; 45 | const background = options.background || "white"; 46 | const data = options.data || {}; 47 | 48 | if (term !== "svg") throw new Error("Only svg term supported for now"); 49 | 50 | script = `set term ${term} enhanced size ${width},${height} background rgb '${background}';set output '${RESULT_FILE}'\n${script}`; 51 | instance.FS.writeFile(SCRIPT_FILE, script); 52 | Object.entries(data).forEach(([name, content]) => 53 | instance.FS.writeFile(name, content) 54 | ); 55 | 56 | const { stdout, exitCode } = exec(SCRIPT_FILE); 57 | 58 | let result = null; 59 | 60 | try { 61 | result = instance.FS.readFile(RESULT_FILE, { encoding: "utf8" }); 62 | } catch (e) {} 63 | 64 | try { 65 | Object.keys(data).forEach((name) => instance.FS.unlink(name)); 66 | instance.FS.unlink(SCRIPT_FILE); 67 | instance.FS.unlink(RESULT_FILE); 68 | } catch (e) {} 69 | 70 | if (exitCode !== 0) throw new Error(stdout); 71 | 72 | return { svg: result, stdout }; 73 | } 74 | 75 | return { version, render, exec, _gnuplot: instance }; 76 | } 77 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # gnuplot compiled to wasm 2 | 3 | Experiment. I took idea from [gnuplot-in-the-browser](https://github.com/CD3/gnuplot-in-the-browser) and tried to make ES6 module distributed in npm. I tried to create minimal API with single usecase in mind - generate SVG and output it as string. 4 | 5 | ```js 6 | import gnuplot from "gnuplot-wasm"; 7 | 8 | const script = ` 9 | set key fixed left top vertical Right noreverse enhanced autotitle box lt black linewidth 1.000 dashtype solid 10 | set samples 1000, 1000 11 | plot [-10:10] sin(x),atan(x),cos(atan(x))`; 12 | 13 | const { render } = await gnuplot(); 14 | const { svg } = render(script); 15 | ``` 16 | 17 | Will produce: 18 | 19 | ![Example of generated graph](./example.svg) 20 | 21 | ## Hack for WASM in Vite 22 | 23 | ```js 24 | import gnuplot from "gnuplot-wasm"; 25 | 26 | import { createRequire } from "module"; 27 | const require = createRequire(import.meta.url); 28 | const wasmPath = require.resolve("gnuplot-wasm/src/gnuplot.wasm"); 29 | 30 | const { render } = await gnuplot({ 31 | locateFile: () => wasmPath, 32 | }); 33 | ``` 34 | 35 | See: 36 | 37 | - https://github.com/vitejs/vite/issues/11694 38 | - https://github.com/httptoolkit/brotli-wasm/issues/8 39 | - https://github.com/sapphi-red/vite-plugin-static-copy/ 40 | 41 | ### Other options 42 | 43 | - [@hpcc-js/wasm](https://www.npmjs.com/package/@hpcc-js/wasm) 44 | - they wrote special function to inline binary WASM file into textual JS file 45 | - they use [fzstd](https://www.npmjs.com/package/fzstd) and [base91](https://github.com/Equim-chan/base91) to decode it later 46 | - [@node-rs/xxhash-wasm32-wasi](https://www.npmjs.com/package/@node-rs/xxhash-wasm32-wasi) 47 | - they have `"main": "xxhash.wasi.cjs",` and ` "browser": "xxhash.wasi-browser.js",` in package.json 48 | - in browser they use `import __wasmUrl from './xxhash.wasm32-wasi.wasm?url'` 49 | - in node they use CJS and `let __wasmFilePath = __nodePath.join(__dirname, 'xxhash.wasm32-wasi.wasm')` 50 | - here [wasm-pack#1334](https://github.com/rustwasm/wasm-pack/issues/1334) they propose to use base64 51 | - node has experiment support for WASM modules [`--experimental-wasm-modules`](https://nodejs.org/api/esm.html#wasm-modules) 52 | - Bun supports [WASM modules](https://bun.sh/docs/bundler/loaders#wasm) 53 | - Vite supports [`?init`](https://vitejs.dev/guide/features#webassembly) for WASM 54 | - plus [vite-plugin-wasm](https://github.com/Menci/vite-plugin-wasm) implements WASM modules 55 | - maybe [import-meta-resolve](https://www.npmjs.com/package/import-meta-resolve) 56 | - [vite#14405](https://github.com/vitejs/vite/discussions/14405) 57 | - `--experimental-import-meta-resolve` 58 | 59 | ## Build 60 | 61 | To build locally you need `docker` and `shell` (plus `tar`, `wget`): 62 | 63 | ```sh 64 | sh build.sh 65 | ``` 66 | 67 | ## TODO 68 | 69 | - smaller size 70 | - `-s MALLOC=emmalloc` 71 | - `-Os -closure 1` 72 | - https://github.com/WebAssembly/wabt 73 | - remove unsued features - can I leave only support for SVG? 74 | - github action for npm deployment 75 | - test in browser 76 | - typescript signatures 77 | - `--emit-tsd gnuplot.d.ts`, but it requires Typescript, which mean I need to create custom Dcoker image with node, npm, TypeScript 78 | -------------------------------------------------------------------------------- /example.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | Gnuplot 10 | Produced by GNUPLOT 5.4 patchlevel 0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -1.5 47 | 48 | 49 | 50 | 51 | -1 52 | 53 | 54 | 55 | 56 | -0.5 57 | 58 | 59 | 60 | 61 | 0 62 | 63 | 64 | 65 | 66 | 0.5 67 | 68 | 69 | 70 | 71 | 1 72 | 73 | 74 | 75 | 76 | 1.5 77 | 78 | 79 | 80 | 81 | -10 82 | 83 | 84 | 85 | 86 | -5 87 | 88 | 89 | 90 | 91 | 0 92 | 93 | 94 | 95 | 96 | 5 97 | 98 | 99 | 100 | 101 | 10 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | sin(x) 113 | 114 | 115 | sin(x) 116 | 117 | 118 | 119 | 245 | 246 | atan(x) 247 | 248 | 249 | atan(x) 250 | 251 | 252 | 253 | 379 | 380 | cos(atan(x)) 381 | 382 | 383 | cos(atan(x)) 384 | 385 | 386 | 387 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | --------------------------------------------------------------------------------