diff --git a/example_test.go b/example_test.go index 542f902..34b74aa 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,7 @@ package vegagoja_test import ( "context" + "fmt" "log" "os" @@ -22,6 +23,259 @@ func Example() { // Output: } +func Example_compile() { + vega := vegagoja.New() + s, err := vega.Compile(candlestickSpec) + if err != nil { + log.Fatal(err) + } + fmt.Println(s) + // Output: + // { + // "$schema": "https://vega.github.io/schema/vega/v5.json", + // "description": "A candlestick chart inspired by an example in Protovis (http://mbostock.github.io/protovis/ex/candlestick.html)", + // "background": "white", + // "padding": 5, + // "width": 400, + // "height": 200, + // "style": "cell", + // "data": [ + // { + // "name": "source_0", + // "url": "data/ohlc.json", + // "format": { + // "type": "json", + // "parse": { + // "date": "date" + // } + // } + // }, + // { + // "name": "data_0", + // "source": "source_0", + // "transform": [ + // { + // "type": "filter", + // "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"low\"]) && isFinite(+datum[\"low\"])" + // } + // ] + // }, + // { + // "name": "data_1", + // "source": "source_0", + // "transform": [ + // { + // "type": "filter", + // "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"open\"]) && isFinite(+datum[\"open\"])" + // } + // ] + // } + // ], + // "marks": [ + // { + // "name": "layer_0_marks", + // "type": "rule", + // "style": [ + // "rule" + // ], + // "from": { + // "data": "data_0" + // }, + // "encode": { + // "update": { + // "stroke": [ + // { + // "test": "datum.open < datum.close", + // "value": "#06982d" + // }, + // { + // "value": "#ae1325" + // } + // ], + // "description": { + // "signal": "\"Date in 2009: \" + (timeFormat(datum[\"date\"], '%m/%d')) + \"; low: \" + (format(datum[\"low\"], \"\")) + \"; high: \" + (format(datum[\"high\"], \"\"))" + // }, + // "x": { + // "scale": "x", + // "field": "date" + // }, + // "y": { + // "scale": "y", + // "field": "low" + // }, + // "y2": { + // "scale": "y", + // "field": "high" + // } + // } + // } + // }, + // { + // "name": "layer_1_marks", + // "type": "rect", + // "style": [ + // "bar" + // ], + // "from": { + // "data": "data_1" + // }, + // "encode": { + // "update": { + // "fill": [ + // { + // "test": "datum.open < datum.close", + // "value": "#06982d" + // }, + // { + // "value": "#ae1325" + // } + // ], + // "ariaRoleDescription": { + // "value": "bar" + // }, + // "description": { + // "signal": "\"Date in 2009: \" + (timeFormat(datum[\"date\"], '%m/%d')) + \"; open: \" + (format(datum[\"open\"], \"\")) + \"; close: \" + (format(datum[\"close\"], \"\"))" + // }, + // "xc": { + // "scale": "x", + // "field": "date" + // }, + // "width": { + // "value": 5 + // }, + // "y": { + // "scale": "y", + // "field": "open" + // }, + // "y2": { + // "scale": "y", + // "field": "close" + // } + // } + // } + // } + // ], + // "scales": [ + // { + // "name": "x", + // "type": "time", + // "domain": { + // "fields": [ + // { + // "data": "data_0", + // "field": "date" + // }, + // { + // "data": "data_1", + // "field": "date" + // } + // ] + // }, + // "range": [ + // 0, + // { + // "signal": "width" + // } + // ], + // "padding": 5 + // }, + // { + // "name": "y", + // "type": "linear", + // "domain": { + // "fields": [ + // { + // "data": "data_0", + // "field": "low" + // }, + // { + // "data": "data_0", + // "field": "high" + // }, + // { + // "data": "data_1", + // "field": "open" + // }, + // { + // "data": "data_1", + // "field": "close" + // } + // ] + // }, + // "range": [ + // { + // "signal": "height" + // }, + // 0 + // ], + // "zero": false, + // "nice": true + // } + // ], + // "axes": [ + // { + // "scale": "x", + // "orient": "bottom", + // "gridScale": "y", + // "grid": true, + // "tickCount": { + // "signal": "ceil(width/40)" + // }, + // "domain": false, + // "labels": false, + // "aria": false, + // "maxExtent": 0, + // "minExtent": 0, + // "ticks": false, + // "zindex": 0 + // }, + // { + // "scale": "y", + // "orient": "left", + // "gridScale": "x", + // "grid": true, + // "tickCount": { + // "signal": "ceil(height/40)" + // }, + // "domain": false, + // "labels": false, + // "aria": false, + // "maxExtent": 0, + // "minExtent": 0, + // "ticks": false, + // "zindex": 0 + // }, + // { + // "scale": "x", + // "orient": "bottom", + // "grid": false, + // "title": "Date in 2009", + // "format": "%m/%d", + // "labelAngle": 315, + // "labelAlign": "right", + // "labelBaseline": "top", + // "labelFlush": true, + // "labelOverlap": true, + // "tickCount": { + // "signal": "ceil(width/40)" + // }, + // "zindex": 0 + // }, + // { + // "scale": "y", + // "orient": "left", + // "grid": false, + // "title": "Price", + // "labelOverlap": true, + // "tickCount": { + // "signal": "ceil(height/40)" + // }, + // "zindex": 0 + // } + // ] + // } +} + func Example_withCSV() { vega := vegagoja.New( vegagoja.WithCSVString(co2Data), @@ -37,164 +291,48 @@ func Example_withCSV() { } const candlestickSpec = `{ - "$schema": "https://vega.github.io/schema/vega/v5.json", - "description": "A candlestick chart inspired by an example in Protovis (http://mbostock.github.io/protovis/ex/candlestick.html)", - "background": "white", - "padding": 5, + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "width": 400, - "height": 200, - "style": "cell", - "data": [ - { - "name": "source_0", - "url": "data/ohlc.json", - "format": {"type": "json", "parse": {"date": "date"}} - }, - { - "name": "data_0", - "source": "source_0", - "transform": [ - { - "type": "filter", - "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"low\"]) && isFinite(+datum[\"low\"])" - } - ] - }, - { - "name": "data_1", - "source": "source_0", - "transform": [ - { - "type": "filter", - "expr": "(isDate(datum[\"date\"]) || (isValid(datum[\"date\"]) && isFinite(+datum[\"date\"]))) && isValid(datum[\"open\"]) && isFinite(+datum[\"open\"])" - } - ] - } - ], - "marks": [ - { - "name": "layer_0_marks", - "type": "rule", - "style": ["rule"], - "from": {"data": "data_0"}, - "encode": { - "update": { - "stroke": [ - {"test": "datum.open < datum.close", "value": "#06982d"}, - {"value": "#ae1325"} - ], - "description": { - "signal": "\"Date in 2009: \" + (timeFormat(datum[\"date\"], '%m/%d')) + \"; low: \" + (format(datum[\"low\"], \"\")) + \"; high: \" + (format(datum[\"high\"], \"\"))" - }, - "x": {"scale": "x", "field": "date"}, - "y": {"scale": "y", "field": "low"}, - "y2": {"scale": "y", "field": "high"} - } + "description": "A candlestick chart inspired by an example in Protovis (http://mbostock.github.io/protovis/ex/candlestick.html)", + "data": {"url": "data/ohlc.json"}, + "encoding": { + "x": { + "field": "date", + "type": "temporal", + "title": "Date in 2009", + "axis": { + "format": "%m/%d", + "labelAngle": -45, + "title": "Date in 2009" } }, - { - "name": "layer_1_marks", - "type": "rect", - "style": ["bar"], - "from": {"data": "data_1"}, - "encode": { - "update": { - "fill": [ - {"test": "datum.open < datum.close", "value": "#06982d"}, - {"value": "#ae1325"} - ], - "ariaRoleDescription": {"value": "bar"}, - "description": { - "signal": "\"Date in 2009: \" + (timeFormat(datum[\"date\"], '%m/%d')) + \"; open: \" + (format(datum[\"open\"], \"\")) + \"; close: \" + (format(datum[\"close\"], \"\"))" - }, - "xc": {"scale": "x", "field": "date"}, - "width": {"value": 5}, - "y": {"scale": "y", "field": "open"}, - "y2": {"scale": "y", "field": "close"} - } - } - } - ], - "scales": [ - { - "name": "x", - "type": "time", - "domain": { - "fields": [ - {"data": "data_0", "field": "date"}, - {"data": "data_1", "field": "date"} - ] - }, - "range": [0, {"signal": "width"}], - "padding": 5 + "y": { + "type": "quantitative", + "scale": {"zero": false}, + "axis": {"title": "Price"} }, - { - "name": "y", - "type": "linear", - "domain": { - "fields": [ - {"data": "data_0", "field": "low"}, - {"data": "data_0", "field": "high"}, - {"data": "data_1", "field": "open"}, - {"data": "data_1", "field": "close"} - ] + "color": { + "condition": { + "test": "datum.open < datum.close", + "value": "#06982d" }, - "range": [{"signal": "height"}, 0], - "zero": false, - "nice": true + "value": "#ae1325" } - ], - "axes": [ + }, + "layer": [ { - "scale": "x", - "orient": "bottom", - "gridScale": "y", - "grid": true, - "tickCount": {"signal": "ceil(width/40)"}, - "domain": false, - "labels": false, - "aria": false, - "maxExtent": 0, - "minExtent": 0, - "ticks": false, - "zindex": 0 - }, - { - "scale": "y", - "orient": "left", - "gridScale": "x", - "grid": true, - "tickCount": {"signal": "ceil(height/40)"}, - "domain": false, - "labels": false, - "aria": false, - "maxExtent": 0, - "minExtent": 0, - "ticks": false, - "zindex": 0 - }, - { - "scale": "x", - "orient": "bottom", - "grid": false, - "title": "Date in 2009", - "format": "%m/%d", - "labelAngle": 315, - "labelAlign": "right", - "labelBaseline": "top", - "labelFlush": true, - "labelOverlap": true, - "tickCount": {"signal": "ceil(width/40)"}, - "zindex": 0 + "mark": "rule", + "encoding": { + "y": {"field": "low"}, + "y2": {"field": "high"} + } }, { - "scale": "y", - "orient": "left", - "grid": false, - "title": "Price", - "labelOverlap": true, - "tickCount": {"signal": "ceil(height/40)"}, - "zindex": 0 + "mark": "bar", + "encoding": { + "y": {"field": "open"}, + "y2": {"field": "close"} + } } ] }` diff --git a/vegagoja.go b/vegagoja.go index f81a1e6..35a9947 100644 --- a/vegagoja.go +++ b/vegagoja.go @@ -27,15 +27,16 @@ type renderFunc func(logger func([]string), spec string, data interface{}, cb fu // Wraps a goja runtime vm, and uses embedded javascript to render the Vega // visualizations. type Vega struct { - r *goja.Runtime - vegaVer func() string - liteVer func() string - vegaRender renderFunc - liteRender renderFunc - logger func(...interface{}) - sources fs.FS - once sync.Once - err error + r *goja.Runtime + vegaVer func() string + liteVer func() string + vegaRender renderFunc + liteRender renderFunc + liteCompile func(string) (string, error) + logger func(...interface{}) + sources fs.FS + once sync.Once + err error } // New creates a new vega instance. @@ -87,6 +88,10 @@ func (vm *Vega) init() error { vm.err = fmt.Errorf("unable to bind vega_render func: %w", err) return } + if err := r.ExportTo(r.Get("vega_lite_compile"), &vm.liteCompile); err != nil { + vm.err = fmt.Errorf("unable to bind vega_lite_compile func: %w", err) + return + } if err := r.ExportTo(r.Get("vega_lite_render"), &vm.liteRender); err != nil { vm.err = fmt.Errorf("unable to bind vega_lite_render func: %w", err) return @@ -106,6 +111,14 @@ func (vm *Vega) Version() (string, string, error) { return vegaVer, liteVer, nil } +// Compile compiles a vega lite specification to a vega specification. +func (vm *Vega) Compile(spec string) (string, error) { + if err := vm.init(); err != nil { + return "", err + } + return vm.liteCompile(spec) +} + // Render renders the spec with the specified data. func (vm *Vega) Render(ctx context.Context, spec string) (res string, err error) { if err = vm.init(); err != nil { diff --git a/vegagoja.js b/vegagoja.js index ac14c81..cc618ea 100644 --- a/vegagoja.js +++ b/vegagoja.js @@ -1,13 +1,5 @@ -function vega_version() { - return vega.version; -} - -function vega_lite_version() { - return vegaLite.version; -} - -function vega_render(logf, spec, loadf, cb) { - const logger = { +function logger(logf) { + return { level(_) {}, error() { logf(["ERROR", ...arguments]); @@ -22,6 +14,17 @@ function vega_render(logf, spec, loadf, cb) { logf(["DEBUG", ...arguments]); }, }; +} + +function vega_version() { + return vega.version; +} + +function vega_lite_version() { + return vegaLite.version; +} + +function vega_render(logf, spec, loadf, cb) { const loader = { load(name, res) { if (res.response != "json" && res.response != "text") { @@ -52,11 +55,12 @@ function vega_render(logf, spec, loadf, cb) { var runtime = vega.parse(s); var view = new vega.View(runtime, { loader: loader, - logger: logger, + logger: logger(logf), logLevel: vega.Debug, }); view.toSVG().then(cb); } catch (e) { + logf(["RENDER ERROR"], e); throw e; } finally { if (view) { @@ -65,15 +69,22 @@ function vega_render(logf, spec, loadf, cb) { } } +function vega_lite_compile(logf, spec) { + const s = vegaLite.compile(JSON.parse(spec), { + logger: logger(logf), + }).spec; + return JSON.stringify(s, null, 2); +} + function vega_lite_render(logf, spec, loadf, cb) { var s = ""; try { - var v = JSON.parse(spec); - s = vegaLite.compile(v).spec; + s = vegaLite.compile(JSON.parse(spec), { + logger: logger(logf), + }).spec; } catch (e) { logf(["COMPILE ERROR", e]); throw e; } - logf(["COMPILED", JSON.stringify(s, null, 2)]); return vega_render(logf, s, loadf, cb); } diff --git a/vegagoja_test.go b/vegagoja_test.go index 66eec89..b6aa43c 100644 --- a/vegagoja_test.go +++ b/vegagoja_test.go @@ -88,7 +88,9 @@ func testRender(t *testing.T, ctx context.Context, testName, name string, timeou case err != nil: t.Fatalf("expected no error, got: %v", err) } - t.Logf("---\n%s\n---", s) + if os.Getenv("VERBOSE") != "" { + t.Logf("---\n%s\n---", s) + } t.Logf("duration: %s", total) if err := os.WriteFile(name+".svg", []byte(s), 0o644); err != nil { t.Fatalf("expected no error, got: %v", err) @@ -105,6 +107,9 @@ func contains(v []string, s string) bool { } var broken = []string{ + "compiled/point_href", + "compiled/scatter_image", + "lite/geo_circle", "lite/point_href", "lite/scatter_image", "vega/contour-plot",