Closed k-groenbroek closed 2 years ago
I played around a bit with this example:
import { View, parse } from "vega"
import { compile } from "vega-lite"
let specVegaLite = {
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
description: "A simple bar chart with embedded data.",
data: {
values: [
{a: "A", b: 28}, {a: "B", b: 55}, {a: "C", b: 43},
{a: "D", b: 91}, {a: "E", b: 81}, {a: "F", b: 53},
{a: "G", b: 19}, {a: "H", b: 87}, {a: "I", b: 52}
]
},
mark: "bar",
encoding: {
x: {field: "a", type: "nominal", axis: {labelAngle: 0}},
y: {field: "b", type: "quantitative"}
}
}
let specVega = compile(specVegaLite).spec
let view = new View(parse(specVega), {renderer: "none"})
view.toSVG().then(content => {
let f = std.open("output.svg", "w")
f.puts(content)
f.close()
})
Exporting to svg works fine with QuickJS. But exporting to png needs a drawing canvas and getting node-canvas package to work with QuickJS is difficult.
QuickJs does give a very lightweight standalone vegalite compiler/parser (<2mb) that can be called from python, interesting to explore further.
Just found out that you used resvg instead of node-canvas. Nice, now we're getting somewhere!
I've attached a minimal standalone example: vegalite example win x64.zip. Run it via
qjs --std index.js
resvg output.svg output.png
So next steps could be:
The python wheel gets a dependency on quickjs, and bundles the js file and relevant resvg binary. The whole thing probably stays under 5mb uncompressed 😎
I'm partial to the option the requires fewer subprocess and IO calls. By using resvg-js
it negates the need to ever save the svg and can instead directly convert the svg string. Currently the altair_saver
method already saves the spec to a json
file in order to get around windows issues with stream sizes.
Additionally, the pre-compiled node addons are simple to include with pkg
. Albeit I kinda force it to include only the one that I want for a given platform.
I hadn't heard of quickjs
and it doesn't seem to be a very active project not to mention quite niche. I'd be concerned about a lack of support if any bugs or edgecases come up.
For what it's worth okab
is already doing significantly better than kaleido
in terms of on disk space usage from the installed package.
For instance on linux:
221M kaleido
54M okab
I don't think something <60 mb is prohibitively large and could be worth the size cost for performance/stability.
Up to you of course! I agree that fewer subprocesses and less IO calls is good. Using https://github.com/PetterS/quickjs there is no subprocess for javascript engine though. I'll see if I can get resvg-js to work, to skip the svg file IO.
Also about QuickJS: It's written by Fabrice Bellard (also creator of ffmpeg) so that should earn it some credits ;)
In order to use resvg-js
you'll need to also package the native node add-on for each platform (see here for how it's loaded).
All the usage examples for PetterS/quickjs
seem simple. Can it handle loading all the vega/d3
bundled code? I've also combined the CLI of both vega/vega-lite
and will expose these options which I don't think are currently well-supported in the selenium or node altair_saver
methods.
Here are all the current options if you invoke okab
from the command line after installing through pip
:
Thanks! I've played around a bit more and I think I have some interesting results.
jsonToSvg
inside the javascript context for converting vega-lite json string to svg string. No need for creating our own bundled js code.jsonToSvg
to get our svg content.We get a compact python package with dependencies on quickjs, svglib, reportlab. There are no subprocess calls or intermediate file IO or packaging binaries. 😎 I have attached a worked out example.zip.
It works and outputs this png:
Thoughts?
Dependency graph looks like:
quickjs 1.19.2 Wrapping the quickjs C library.
reportlab 3.6.11 The Reportlab Toolkit
└── pillow >=9.0.0
svglib 1.4.1 A pure-Python library for reading and converting SVG
├── cssselect2 >=0.2.0
│ ├── tinycss2 *
│ │ └── webencodings >=0.4
│ └── webencodings * (circular dependency aborted here)
├── lxml *
├── reportlab *
│ └── pillow >=9.0.0
└── tinycss2 >=0.6.0
└── webencodings >=0.4
Thanks for looking into this more and I played around with the example you attached.
In no particular order some observations:
conda-forge
which would mean pyquickjs
would also need to be submitted which wouldn't be to much of an issue.Pyquickjs
installed from source distribution on my machine fails to compile with obscure errors about ld
shared objects.okab
can be distributed as a standalone binary with no python/node dependencies, however with the pyquickjs
any CLI will depend on a python distribution and these dependencies. Admittedly, the number of users interested in a standalone binary to convert vega
/vega-lite
specs to static figures is likely very small.quickjs
or if we could modify the js
to make it work. Not having dynamically loaded data isn't a dealbreaker for me but others users may disagree.Finally I stumbled on the biggest issue with using svglib
and reportlab
that I think excludes them all together.
As you can see with for example this chart that it fails to render gradients. Which seems like a pretty common use case.
Hexbin Svg:
Hexbin Png w/Pyquickjs
& svglib
:
PNG from Current Version of Okab:
Thanks! Too bad.. indeed you are right that svglib + reportlab don't support rendering gradients, so that won't work for us. I saw that resvg uses the tiny-skia rendering engine. I've tried to play around with skia-python but couldn't get any fonts to render.
Then I saw there is also a quickjs-rs. So we could write a small wrapper in Rust that parses vega to svg via quickjs, then renders svg via resvg. That can be compiled to a standalone binary, or made into a Python package via pyo3. I'm pretty sure it ticks all the boxes.
Haha, and then I found this https://github.com/vega/vl-convert, which does exactly that but using deno instead of quickjs. I'll open an issue over there to link the discussions. Do you think there is value in combining forces?
I'll close this in favor of the issues linked above. Thanks!
Thanks for working on this! I haven't put in a lot of effort thinking about it, but maybe QuickJS can be used to make an even smaller rendering engine? It's a whole Javascript engine in <1mb, see https://github.com/bellard/quickjs and https://bellard.org/quickjs/. There are precompiled binaries for most common platforms.
I've done something like:
From Python perspective, the quickjs engine and javascript file would be static assets. The python code is then responsible for running quickjs with the javascript file. That may be further simplified with https://github.com/PetterS/quickjs.
What are your thoughts?