altair
altair copied to clipboard
[Feature Request] Simplify saving to PNG
As a new user looking for better grammar and control than pandas and matplotlib, this has been my adoption experience with Altair:
- Great documentation, able to start plotting right away.
- Support for faceting; nice, I will commit to learning this "language".
- Go to save to PNG.
- Need other pip package
altair_saver; install it. - Need
geckodriverfrom package manager; install it. - Got chromedriver error and had to search outside documentation for solution.
- Even after sorting that, encountered deprecation error.
Problem is I need two dependencies for it to do something I would expect it to
support natively: save as PNG. These two dependencies are not able to be tracked
as dependencies as far as I know, meaning if I pip uninstall altair I'd like
it to take altair_saver and geckodriver with it. I would accept needing just
altair and altair_saver from pip.
Further, once I got the dependencies sorted, I had to search outside the
instructions to learn that if I'm using geckodriver I need to call save with
save(..., webdriver="firefox").
Even after overcoming that, I got hit with an error due to a recent selenium upgrade with breaking dependencies, as noted in this issue.
Sure if I'm making one plot, it's not too bad to output .html, view it in firefox, and save to PNG using the dot menu. The problem is it's never just one plot. I usually have a program that analyzes my dataframe and generates many plots which I'd like to name then and there based on what I'm doing then and there in the program.
A potential solution would be providing an Altair package or script, or linking to an existing pip package that lets me output a directory of .html plots, and convert them all to .png, preserving the basenames.
Best case scenario: chart.save("to.png") just works.
I'm taking the time to write this to improve this otherwise powerful, promising tool, and possibly, adoption thereof. I turned away from Altair simply because the hurdle to save to PNG was disproportionately and unexpectedly large. I came back because pandas and matplotlib is too restricting and indirect, and the stifling of this became significantly clearer after my brief work with Altair. It's worth noting I also tried plotnine in my search, and I found Altair's documentation to be better, and syntax to be much more direct. Altair had more pleasing default colors and conveniently used labeling from the df, without intervention.
Related: https://github.com/altair-viz/altair/issues/2453.
So far my attempt at workaround is a CutyCapt script:
#!/bin/sh
if [[ $# -eq 0 ]]
then
echo "No arguments were provided. Specify files to convert."
exit 1
fi
for file in "$@"
do
if [[ "$file" == *".html" ]]
then
in=`realpath $file`
out=${in/".html"/".png"}
/usr/bin/CutyCapt --url=file://$in --out=$out
echo "`basename $in` -> `basename $out`"
fi
done
xclip -o > /usr/local/bin/htmltopng
htmltopng dir_of_html_charts/*
It seems CutyCapt can't parse an Altair html as the output is blank. Help needed.
Thanks for documenting your approach. As you know now, to get html is easy, to get png is hard..
We've known the current approach is terrible for years, but the current approach is what we've found to be possible (and if I dare say, its better than nothing!) If you have ideas about technologies that could make it easier, we're all ears.
No other suggestions apart from the potential solutions you referenced in https://github.com/altair-viz/altair/issues/2453 and https://github.com/altair-viz/altair_saver/issues/85.
Why is it so straightforward for matplotlib and ggplot (plotnine)?
Any ideas why my script isn't working? I'd be fine moving forward with that.
Why is it so straightforward for matplotlib and ggplot (plotnine)?
Because their rendering stacks are built on codebases that can be distributed via wheels and executed natively on multiple platforms, whereas Altair's rendering stack is built on Javascript, which cannot be distributed via wheels and requires a browser process for execution. Further, because of very valid security concerns, browsers deliberately make it difficult to programmatically execute code in their runtimes.
Any ideas why my script isn't working?
I'm not familiar with CutyCapt, so no I don't have any ideas about what's going wrong.
Thank you very much for explaining.
Because their rendering stacks are built on codebases that can be distributed via wheels and executed natively on multiple platforms, whereas Altair's rendering stack is built on Javascript
Are there any plans to address the issue at this level?
I'm not familiar with CutyCapt, so no I don't have any ideas about what's going wrong.
What is your workflow for getting out a png?
Are there any plans to address the issue at this level?
No, I don't know of any plans to build a Vega-Lite renderer that does not depend on the Node/Javascript ecosystem.
What is your workflow for getting out a png?
I use the process documented here: https://altair-viz.github.io/user_guide/saving_charts.html#png-svg-and-pdf-format
Thank you. Isn't that method broken as of https://github.com/altair-viz/altair_saver/issues/104?
Yes, the selenium version appears to be broken currently if you have a recent version of selenium installed.
Okay full circle for me. Thanks for the help!
Is there anyway to set the webdriver parameter one time so that all save("chart.png") calls behave like save("chart.png", webdriver="firefox")?
I couldn't find this parameter documented on the site.
I have come up with a proof-of-concept for something that may be usable for this at the cost of distribution size and build complexity. But with the added benefit of no selenium or node just a pip installable wheel. There is an example wheel for linux-x64 in the releases.
TLDR: We compile vega and a renderer (but not node-canvas) into native binaries then package them up with altair_saver.
Update: New release has some additional wheels (linux-glibc, win-x64, and macos-x64) folks can try locally, also fixed the issue with windows.
Interesting @daylinmorgan! Thanks for working on this.
I did some tests on my local machine (mac) and it worked out of the box. Nice!
I did some tests with the scaling factor and for this spec the timings were scaling n² until scale_factor=7 , afterwards it jumped up. Maybe the scale_factor timing will be different when using the resvg scaling options instead as the vega-embed option?

On Google Colab I noticed that text and labels are not captured in the saved png: https://colab.research.google.com/drive/1P__G39EAkoXDScR4AGtHi21u7z1uAXTu?usp=sharing

30mb for a rendering engine is not large! Small enough to be discussed to be integrated within Altair itself..
@daylinmorgan Big thanks for working on this! I don't know what would be regarded small/straightforward enough to be distributed with altair, but it is really exciting to see progress being made in this area!
I mistakenly thought the scale factor was only an option in the toCanvas() method. But quickly testing locally it doesn't seem first scaling with vega with makes a huge difference in overall time for large scaling factor's.
# with resvg-based scale
node ../index --spec example-wind.json --opts --format png --> 161.93s user 59.46s system 98% cpu 3:43.77 total
# with vega-based scale
node ../index --spec example-wind.json --opts --format png --> 155.30s user 58.09s system 99% cpu 3:35.46 total
# with vega-lite cli
vl2png example-wind.json --scale 15 > test.png --> 2.43s user 0.05s system 108% cpu 2.289 total
Eitherway I think bottleneck here is 'resvg-js`.
resvg-js/resvg must not be picking up the system fonts in google collab for some reason, but they are definitely there.

Update: New release has some additional wheels (linux-glibc, win-x64, and macos-x64) folks can try locally, also fixed the issue with windows.
I think this is a much better workaround, if altair_saver continue not updating, use this package as default save method will be good news.
For anyone interested I've shifted work on this over to a separate package okab which you can now install directly from pypi. Additionally, you can invoke the binary directly to convert vega/vega-lite jsons to svg/png. I'm gonna focus on creating a standalone saver that doesn't require altair_saver at all. I don't know that it would make the most sense to deploy within the main altair package, rather than as an optional dependency similar to altair_saver.
Thanks for the update @daylinmorgan! In fact, I already use okab as part of a GitHub Action workflow, be careful before you realize you have created critical software;)!
Direct integration within altair is probably too quick, but another option would be to include it as a repository within https://github.com/altair-viz.
I like the plan to make it work without altair_saverš.
Related tweet mentioning the possibility to for simplified vega-lite image export as part of a new vl convert package via deno https://twitter.com/jonmmease/status/1579529683900989440?s=20&t=uDxBGngqWXaaiaXjT9OdKw
@joelostblom When starting to play around with this idea of embedding vega in a python package for okab my original goal was to use deno since it can compile typescript/javascript down to a single binary. But as @jonmmease mentions there a issues using node-canvas with deno. Hence why I switched to resvg-js to handle static image rendering and instead packaged all the javascript using pkg because it's node package relies on native add-on's. I'd like to explore deno again if resvg-js manages to add font support to their WASM library.
Hi all š In case you're interested, here is a PR where I'm adding support for SVG and PNG export to VlConvert: https://github.com/jonmmease/vl-convert/pull/8/.
There's a lot in common with @daylinmorgan's okab approach (I'm also using resvg), but the architecture is fairly different given how VlConvert is using Deno embedded in Rust. I think I had a breakthrough in terms of how to get accurate SVG text placement without node canvas (overriding Vega's text width calculation function with a custom Rust function that computes the width using usvg). Would love any feedback you all have!
As of version 0.3.0, vl-convert-python supports dependency-free SVG and PNG static image export for Vega and Vega-Lite chart specifications.
You can give it a try on Binder
There's no specific integration with Altair right now, but I'd be happy to talk about what that could look like if the community finds that this approach works well for them. I'd also be happy to move the project to the Vega or Altair GitHub org if folks are interested in that.
Awesome! I think it's wonderful to see all the work currently being done on simplifying the export workflow, really exciting!. Whether a project is suitable to be transferred into the Altair or Vega projects will be up to @jakevdp and @domoritz so pinging them here.
Vega probably makes more sense. If you give me admin permissions, I can move it.
Vega probably makes more sense. If you give me admin permissions, I can move it.
Sounds good! I sent you an invite and will make you an admin after you accept.
The approach taken by vl-convert works well and is likely more stable than relying on vercel/pkg as okab currently does not to mention the resulting wheels are slightly smaller thanks to the slimmer deno runtime without what is essentially an embedded file system in pkg's case.
Ideally altair's native save function could attempt to load vl-convert once the API is stable and use it's conversion functions before falling back on altair_saver. But the rate of development on altair has been quite slow so it may be some time before something like that could be merged. In the mean time it might be nice to port over some of the save logic into vl-convert to save the user having to specify things like vega vs vega-lite or version number. Though I doubt many people using altair are writing vega specs these days. So in other words a smaller wrapper function around the lower-level conversion functions could be useful.