Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify saving to PNG #2662

Closed
mcp292 opened this issue Jul 30, 2022 · 33 comments
Closed

Simplify saving to PNG #2662

mcp292 opened this issue Jul 30, 2022 · 33 comments

Comments

@mcp292
Copy link

mcp292 commented Jul 30, 2022

As a new user looking for better grammar and control than pandas and
matplotlib, this has been my adoption experience with Altair:

  1. Great documentation, able to start plotting right away.
  2. Support for faceting; nice, I will commit to learning this "language".
  3. Go to save to PNG.
  4. Need other pip package altair_saver; install it.
  5. Need geckodriver from package manager; install it.
  6. Got chromedriver error and had to search outside documentation for solution.
  7. 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.

@mcp292
Copy link
Author

mcp292 commented Jul 30, 2022

Related: #2453.

@mcp292
Copy link
Author

mcp292 commented Jul 31, 2022

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.

@mattijn
Copy link
Contributor

mattijn commented Jul 31, 2022

Thanks for documenting your approach. As you know now, to get html is easy, to get png is hard..

@jakevdp
Copy link
Collaborator

jakevdp commented Aug 1, 2022

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.

@mcp292
Copy link
Author

mcp292 commented Aug 1, 2022

No other suggestions apart from the potential solutions you referenced in #2453 and altair-viz/altair_saver#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.

@jakevdp
Copy link
Collaborator

jakevdp commented Aug 1, 2022

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.

@mcp292
Copy link
Author

mcp292 commented Aug 1, 2022

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?

@jakevdp
Copy link
Collaborator

jakevdp commented Aug 1, 2022

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

@mcp292
Copy link
Author

mcp292 commented Aug 1, 2022

Thank you. Isn't that method broken as of altair-viz/altair_saver#104?

@jakevdp
Copy link
Collaborator

jakevdp commented Aug 1, 2022

Yes, the selenium version appears to be broken currently if you have a recent version of selenium installed.

@mcp292
Copy link
Author

mcp292 commented Aug 1, 2022

Okay full circle for me. Thanks for the help!

@mcp292
Copy link
Author

mcp292 commented Aug 2, 2022

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.

@mcp292
Copy link
Author

mcp292 commented Aug 2, 2022

Workaround.

@daylinmorgan
Copy link
Contributor

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.

@daylinmorgan
Copy link
Contributor

Update: New release has some additional wheels (linux-glibc, win-x64, and macos-x64) folks can try locally, also fixed the issue with windows.

@mattijn
Copy link
Contributor

mattijn commented Aug 21, 2022

Interesting @daylinmorgan! Thanks for working on this.

@mattijn
Copy link
Contributor

mattijn commented Aug 22, 2022

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 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?
image

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
image

30mb for a rendering engine is not large! Small enough to be discussed to be integrated within Altair itself..

@joelostblom
Copy link
Contributor

joelostblom commented Aug 22, 2022

@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!

@daylinmorgan
Copy link
Contributor

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.
image

@PaleNeutron
Copy link

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.

@daylinmorgan
Copy link
Contributor

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.

@mattijn
Copy link
Contributor

mattijn commented Sep 8, 2022

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👍.

@joelostblom
Copy link
Contributor

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

@daylinmorgan
Copy link
Contributor

@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.

@jonmmease
Copy link
Contributor

Hi all 👋 In case you're interested, here is a PR where I'm adding support for SVG and PNG export to VlConvert: vega/vl-convert#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!

@jonmmease
Copy link
Contributor

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
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.

@joelostblom
Copy link
Contributor

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.

@domoritz
Copy link
Member

Vega probably makes more sense. If you give me admin permissions, I can move it.

@jonmmease
Copy link
Contributor

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.

@daylinmorgan
Copy link
Contributor

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.

@mattijn
Copy link
Contributor

mattijn commented Oct 19, 2022

Very much open for PRs to improve this experience!

@jonmmease
Copy link
Contributor

Thanks for the kind words on vl-convert @daylinmorgan, and for your work on okab.

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

This would be my preference, and I'd be happy to work on a PR for this soon

@joelostblom
Copy link
Contributor

This has been implemented in #2701

@joelostblom joelostblom changed the title [Feature Request] Simplify saving to PNG Simplify saving to PNG Apr 12, 2024
@joelostblom joelostblom moved this to Static image export in Roadmap Apr 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Static image export
Development

No branches or pull requests

8 participants