Skip to content

Instantly share code, notes, and snippets.

@etpinard
Last active August 17, 2021 15:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etpinard/26731a74bd746bc471ffeeafdc58612b to your computer and use it in GitHub Desktop.
Save etpinard/26731a74bd746bc471ffeeafdc58612b to your computer and use it in GitHub Desktop.
Use electron to export plotly.js graphs as images
"Use electron to export plotly.js graphs as images."
node_modules
npm-debug.log*
fig.png

electron-plotly.js

Use electron to export plotly.js graphs as images.

How to run this thing?

  • npm i
  • npm start

If you're on Ubuntu (or maybe some other Linux distro) and you have xvfb installed, you can run this things headlessly (no joke) with:

npm run start:headless

and see results in fig.png.

You can also pass as argument any URL to a plotly "data"/"layout" JSON or the name of the plotly.js test mock you'd like to export. For example:

npm start -- gl3d_bunny

How does this thing work?

Using electron the main module sends plotly.js "data"/"layout" JSON data to the renderer module. The renderer calls Plotly.plot and Plotly.toImage in succession and then sends the image data back to the main module it writes a PNG file.

Some useful articles:

#!/usr/bin/env node
const path = require('path')
const spawn = require('child_process').spawn
const electronPath = require('electron')
const args = process.argv.slice(2)
args.unshift(path.resolve(path.join(__dirname, 'main.js')))
const electron = spawn(electronPath, args, {
stdio: ['inherit', 'inherit', 'pipe', 'ipc']
})
electron.stderr.on('data', d => {
if (!d.toString('utf8').match(/^\[\d+:\d+/)) {
process.stderr.write(d)
}
})
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>plotly.js in electron</title>
<script src="node_modules/plotly.js/dist/plotly.js"></script>
</head>
<body>
<script src="renderer.js"></script>
</body>
</html>
const {app, BrowserWindow} = require('electron')
const {ipcMain} = require('electron')
const fs = require('fs')
const https = require('https')
const url = require('url')
const DEBUG = !!process.env.DEBUG
const IS_TEST = process.env.NODE_ENV === 'test'
const ARG = process.argv[2] || '0'
const URL = url.parse(ARG).host
? ARG
: `https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/${ARG}.json`
let win
// Taken https://developers.google.com/web/updates/2017/04/headless-chrome
app.commandLine.appendSwitch('headless')
app.commandLine.appendSwitch('disable-gpu')
app.commandLine.appendSwitch('remote-debugging-port', '9222')
// app.commandLine.appendSwitch('ignore-gpu-blacklist')
app.on('ready', () => {
win = new BrowserWindow({
height: 1024,
width: 1024
})
win.loadURL(`file://${__dirname}/index.html`)
if (DEBUG) {
win.openDevTools()
}
win.on('closed', () => {
win = null
})
win.webContents.on('did-finish-load', () => {
wget(URL, (err, body) => {
const fig = JSON.parse(body)
win.webContents.send('fig', fig)
})
})
})
ipcMain.on('img', (event, arg) => {
const done = () => {
app.emit('done')
if (!IS_TEST) app.quit()
}
switch(arg.code) {
case 200:
const buf = new Buffer(arg.imgData, 'base64')
fs.writeFile(`${__dirname}/fig.png`, buf, (err) => {
if (err) throw err
done()
})
break
case 525:
console.warn(`plotly.js error: ${arg.msg}`)
done()
break
}
})
function wget (URL, cb) {
let body = ''
https.get(URL, res => {
res.on('data', chunk => body += chunk)
res.on('end', () => cb(null, body))
})
.on('error', err => cb(err))
}
{
"name": "electron-plotly.js",
"version": "1.0.0",
"description": "Use electron to export plotly.js graphs as images",
"main": "main.js",
"scripts": {
"start": "electron .",
"start:debug": "DEBUG=1 npm start",
"start:headless": "export DISPLAY=:99 && xvfb-run -a -e /dev/stdout --server-args='-screen 0, 1024x1024x24' npm start",
"test": "tap test.js"
},
"keywords": [],
"author": "Étienne Tétreault-Pinard",
"license": "MIT",
"dependencies": {
"electron": "^1.8.1",
"hat": "0.0.3",
"plotly.js": "^1.31.0"
},
"devDependencies": {
"spectron": "^3.7.2",
"tap": "^10.7.1"
}
}
/* global Plotly:false */
const {ipcRenderer} = require('electron')
const hat = require('hat')
ipcRenderer.on('fig', (event, fig) => {
const uid = hat()
const gd = document.createElement('div')
gd.id = uid
document.body.appendChild(gd)
Plotly.newPlot(gd, fig)
.then(gd => Plotly.toImage(gd))
.then(imgData => {
ipcRenderer.send('img', {
code: 200,
imgData: imgData.replace(/^data:image\/\w+;base64,/, '')
})
Plotly.purge(gd)
document.body.removeChild(gd)
})
.catch(err => {
ipcRenderer.send('img', {
code: 525,
msg: JSON.stringify(err, ['message', 'arguments', 'type', 'name'])
})
})
})
const tap = require('tap')
const fs = require('fs')
const Application = require('spectron').Application
const electron = require('electron')
const app = new Application({
path: electron,
args: ['.', '10'],
env: {NODE_ENV: 'test'}
})
const output = `${__dirname}/fig.png`
tap.test('should launch + create *fig.png*', t => {
t.plan(3)
if (fs.existsSync(output)) {
fs.unlinkSync(output)
}
t.notOk(fs.existsSync(output))
app.start().then(() => {
app.client.getWindowCount().then(function (count) {
t.equal(count, 1)
})
})
.catch((err) => t.fail(err))
setTimeout(() => {
t.ok(fs.existsSync(output))
}, 3000)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment