Skip to content

Commit

Permalink
feat: node only cli (#6881)
Browse files Browse the repository at this point in the history
fixes #6863
  • Loading branch information
domoritz committed Sep 21, 2020
1 parent 7ea3872 commit b73ad1c
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 68 deletions.
56 changes: 56 additions & 0 deletions bin/args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module.exports = function (type) {
const helpText = `${type === 'vega' ? 'Compile' : 'Render'} a Vega-Lite specification to ${
type === 'vega' ? 'Vega' : type.toUpperCase()
}.
Usage: vl2${type} [vega_json_spec_file] [output_${type}_file]
If no arguments are provided, reads from stdin.
If output_${type}_file is not provided, writes to stdout.
For errors and log messages, writes to stderr.
To load data, you may need to set a base directory:
For web retrieval, use '-b http://host/data/'.
For files, use '-b file:///dir/data/' (absolute) or '-b data/' (relative).`;

const args = require('yargs').usage(helpText).demand(0);

args
.string('b')
.alias('b', 'base')
.describe('b', 'Base directory for data loading. Defaults to the directory of the input spec.');

args
.string('l')
.alias('l', 'loglevel')
.describe('l', 'Level of log messages written to stderr. One of "error", "warn" (default), "info", or "debug".');

args
.string('c')
.alias('c', 'config')
.describe('c', 'Vega config object. Either a JSON file or a .js file that exports the config object.');

args
.string('f')
.alias('f', 'format')
.describe('f', 'Number format locale descriptor. Either a JSON file or a .js file that exports the locale object.');

args
.string('t')
.alias('t', 'timeFormat')
.describe(
't',
'Date/time format locale descriptor. Either a JSON file or a .js file that exports the locale object.'
);

if (type === 'svg') {
args.boolean('h').alias('h', 'header').describe('h', 'Include XML header and SVG doctype.');
}

args.number('s').alias('s', 'scale').default('s', 1).describe('s', 'Output resolution scale factor.');

args.number('seed').describe('seed', 'Seed for random number generation.');

if (type === 'vega') {
args.boolean('p').alias('p', 'pretty').describe('p', 'Output human readable/pretty spec.');
}

return args.help().version().argv;
};
21 changes: 21 additions & 0 deletions bin/read.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// from vega-cli

const {createReadStream} = require('fs');

module.exports = file => {
return new Promise((resolve, reject) => {
const input = file ? createReadStream(file) : process.stdin;
let text = '';

input.setEncoding('utf8');
input.on('error', err => {
reject(err);
});
input.on('data', chunk => {
text += chunk;
});
input.on('end', () => {
resolve(text);
});
});
};
65 changes: 65 additions & 0 deletions bin/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// modified from vega-cli

const vega = require('vega');
const path = require('path');
const args = require('./args');
const read = require('./read');
const vegaLite = require('..');

function load(file) {
return require(path.resolve(file));
}

const Levels = {
error: vega.Error,
warn: vega.Warn,
info: vega.Info,
debug: vega.Debug
};

module.exports = function (type, callback, opt) {
// parse command line arguments
const arg = args(type);

// set baseURL, if specified. default to input spec directory
const base = arg.base || (arg._[0] ? path.dirname(arg._[0]) : null);

// set log level, defaults to logging warning messages
const loglevel = Levels[String(arg.loglevel).toLowerCase()] || vega.Warn;

// load config file, if specified
const config = arg.config ? load(arg.config) : null;

// set output image scale factor
const scale = arg.scale || undefined;

// use a seeded random number generator, if specified
if (typeof arg.seed !== 'undefined') {
if (Number.isNaN(arg.seed)) throw 'Illegal seed value: must be a valid number.';
vega.setRandom(vega.randomLCG(arg.seed));
}

// locale options, load custom number/time formats if specified
const locale = {
number: arg.format ? load(arg.format) : null,
time: arg.timeFormat ? load(arg.timeFormat) : null
};

// instantiate view and invoke headless render method
function render(vlSpec) {
const vgSpec = vegaLite.compile(vlSpec).spec;
const view = new vega.View(vega.parse(vgSpec, config), {
locale: locale, // set locale options
loader: vega.loader({baseURL: base}), // load files from base path
logger: vega.logger(loglevel, 'error'), // route all logging to stderr
renderer: 'none' // no primary renderer needed
}).finalize(); // clear any timers, etc

return (type === 'svg' ? view.toSVG(scale) : view.toCanvas(scale, opt)).then(_ => callback(_, arg));
}

// read input from file or stdin
read(arg._[0] || null)
.then(text => render(JSON.parse(text)))
.catch(err => console.error(err)); // eslint-disable-line no-console
};
22 changes: 16 additions & 6 deletions bin/vl2pdf
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
#!/bin/bash
#!/usr/bin/env node

# Expects the path to a Vega-Lite specification as the first argument.
# Passes remaining arguments to vg2pdf using npx.
// Render a Vega-Lite specification to PDF, using node canvas
const {createWriteStream} = require('fs');
const render = require('./render');

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
render(
'pdf',
function (canvas, arg) {
const file = arg._[1] || null;
const out = file ? createWriteStream(file) : process.stdout;
const stream = canvas.createPDFStream();
stream.on('data', chunk => {
out.write(chunk);
});
},
{type: 'pdf', context: {textDrawingMode: 'glyph'}}
);

# only passes the first argument to vl2vg
$DIR/vl2vg $1 | npx -p vega vg2pdf '' ${@:2}
17 changes: 10 additions & 7 deletions bin/vl2png
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/bin/bash
#!/usr/bin/env node

# Expects the path to a Vega-Lite specification as the first argument.
# Passes remaining arguments to vg2png using npx.
// Render a Vega-Lite specification to PNG, using node canvas
const {createWriteStream} = require('fs');
const render = require('./render');

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# only passes the first argument to vl2vg
$DIR/vl2vg $1 | npx -p vega vg2png '' ${@:2}
render('png', function(canvas, arg) {
const file = arg._[1] || null;
const out = file ? createWriteStream(file) : process.stdout;
const stream = canvas.createPNGStream();
stream.on('data', chunk => { out.write(chunk); });
});
28 changes: 22 additions & 6 deletions bin/vl2svg
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
#!/bin/bash
#!/usr/bin/env node

# Expects the path to a Vega-Lite specification as the first argument.
# Passes remaining arguments to vg2svg using npx.
// Render a Vega-Lite specification to SVG
const {writeFile} = require('fs');
const render = require('./render');

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
const svgHeader =
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ' +
'"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n';

# only passes the first argument to vl2vg
$DIR/vl2vg $1 | npx -p vega vg2svg '' ${@:2}
render('svg', function (body, arg) {
const svg = (arg.h ? svgHeader : '') + body;
const file = arg._[1] || null;

if (file) {
// write to file
writeFile(file, svg, err => {
if (err) throw err;
});
} else {
// write to stdout
process.stdout.write(svg);
}
});
58 changes: 9 additions & 49 deletions bin/vl2vg
Original file line number Diff line number Diff line change
@@ -1,65 +1,25 @@
#!/usr/bin/env node
// Compile a Vega-Lite spec to Vega

//@ts-check
'use strict';

const helpText = `Compile a Vega-Lite spec to Vega.

Usage: vl2vg [vega_lite_json_file] [output_vega_json_file] [-p]
If no arguments are provided, reads from stdin.
If output_vega_json_file is not provided, writes to stdout.
Passing -p formats the generated Vega spec.`;
// Compile a Vega-Lite spec to Vega

// import required libraries
const {createReadStream, createWriteStream} = require('fs');
const vegaLite = require('../build/vega-lite');
const {createWriteStream} = require('fs');
const vegaLite = require('..');
const compactStringify = require('json-stringify-pretty-compact');
const read = require('./read');
const args = require('./args');

// arguments
const args = require('yargs')
.usage(helpText)
.demand(0);

args
.boolean('p')
.alias('p', 'pretty')
.describe('p', 'Output human readable/pretty spec.');

const argv = args.help().version().argv;

/**
* Read a file.
*
* @param file {string} File path
*/
function read(file) {
return new Promise((resolve, reject) => {
const input = file ? createReadStream(file) : process.stdin;
let text = '';

input.setEncoding('utf8');
input.on('error', err => reject(err));
input.on('data', chunk => (text += chunk));
input.on('end', () => resolve(text));
});
}
const arg = args('vega');

// load spec, compile vg spec
read(argv._[0]).then(text => compile(JSON.parse(text)));
read(arg._[0]).then(text => compile(JSON.parse(text)));

/**
* Compile the Vega-Lite spec to Vega.
*
* @param vlSpec {import("../src").TopLevelSpec} The Vega-Lite spec.
*/
function compile(vlSpec) {
// @ts-ignore
const vgSpec = vegaLite.compile(vlSpec).spec;

const file = argv._[1] || null;
const file = arg._[1] || null;
const out = file ? createWriteStream(file) : process.stdout;
if (argv.p) {
if (arg.p) {
out.write(compactStringify(vgSpec) + '\n');
} else {
out.write(JSON.stringify(vgSpec) + '\n');
Expand Down

0 comments on commit b73ad1c

Please sign in to comment.