plotly.js icon indicating copy to clipboard operation
plotly.js copied to clipboard

Allow plotly.py to define custom modebar buttons.

Open CmpCtrl opened this issue 4 years ago • 4 comments

First off, I don't know javascript well at all, and haven't dug into plotly.js so I don't really know how challenging this request is, but it seems simple from here.

I would like to be able to define a custom modebar button and I am using plotly.py so it appears its not currently possible, see plotly.py issue 2114. I realize I should be using plotly.js directly, but this seems like an easy enough feature to expose to plotly.py. Anyway, as far as I can tell its not possible to define the click function of a custom button from plotly.py since it dumps the config dictionary to json which of course adds double quotes around each field, ie "click": "function (gd) ...". It seems like plotly.js then interprets that as a string instead of javascript and throws an error must provide button 'click' function in button config. If i edit the html to remove the quotes around that field it works as expected, ie "click": function (gd) ....

Is it possible to edit the createButton function to also accept a string and interpret that as .js? Or, is there a better way to accomplish this? My workaround for the time being will be to edit the html after plotly.py generates it.

For reference, here is a more complete config dictionary dumped to json.

{
 "modeBarButtonsToAdd": [
  {
   "name": "Copy to Clipboard",
   "icon": {
    "path": "M102.17,29.66A3,3,0,0,0,100,26.79L73.62,1.1A3,3,0,0,0,71.31,0h-46a5.36,5.36,0,0,0-5.36,5.36V20.41H5.36A5.36,5.36,0,0,0,0,25.77v91.75a5.36,5.36,0,0,0,5.36,5.36H76.9a5.36,5.36,0,0,0,5.33-5.36v-15H96.82a5.36,5.36,0,0,0,5.33-5.36q0-33.73,0-67.45ZM25.91,20.41V6h42.4V30.24a3,3,0,0,0,3,3H96.18q0,31.62,0,63.24h-14l0-46.42a3,3,0,0,0-2.17-2.87L53.69,21.51a2.93,2.93,0,0,0-2.3-1.1ZM54.37,30.89,72.28,47.67H54.37V30.89ZM6,116.89V26.37h42.4V50.65a3,3,0,0,0,3,3H76.26q0,31.64,0,63.24ZM17.33,69.68a2.12,2.12,0,0,1,1.59-.74H54.07a2.14,2.14,0,0,1,1.6.73,2.54,2.54,0,0,1,.63,1.7,2.57,2.57,0,0,1-.64,1.7,2.16,2.16,0,0,1-1.59.74H18.92a2.15,2.15,0,0,1-1.6-.73,2.59,2.59,0,0,1,0-3.4Zm0,28.94a2.1,2.1,0,0,1,1.58-.74H63.87a2.12,2.12,0,0,1,1.59.74,2.57,2.57,0,0,1,.64,1.7,2.54,2.54,0,0,1-.63,1.7,2.14,2.14,0,0,1-1.6.73H18.94a2.13,2.13,0,0,1-1.59-.73,2.56,2.56,0,0,1,0-3.4ZM63.87,83.41a2.12,2.12,0,0,1,1.59.74,2.59,2.59,0,0,1,0,3.4,2.13,2.13,0,0,1-1.6.72H18.94a2.12,2.12,0,0,1-1.59-.72,2.55,2.55,0,0,1-.64-1.71,2.5,2.5,0,0,1,.65-1.69,2.1,2.1,0,0,1,1.58-.74ZM17.33,55.2a2.15,2.15,0,0,1,1.59-.73H39.71a2.13,2.13,0,0,1,1.6.72,2.61,2.61,0,0,1,0,3.41,2.15,2.15,0,0,1-1.59.73H18.92a2.14,2.14,0,0,1-1.6-.72,2.61,2.61,0,0,1,0-3.41Zm0-14.47A2.13,2.13,0,0,1,18.94,40H30.37a2.12,2.12,0,0,1,1.59.72,2.61,2.61,0,0,1,0,3.41,2.13,2.13,0,0,1-1.58.73H18.94a2.16,2.16,0,0,1-1.59-.72,2.57,2.57,0,0,1-.64-1.71,2.54,2.54,0,0,1,.65-1.7ZM74.3,10.48,92.21,27.26H74.3V10.48Z",
    "transform": "scale(0.12)"
   },
   "click": "function (gd) {Plotly.toImage(gd, { format: 'png', width: 2100, height: 900 }).then(async function (url) {try {const data = await fetch(url);const blob = await data.blob();await navigator.clipboard.write([new ClipboardItem({[blob.type]: blob})]);console.log('Image copied.');} catch (err) {console.error(err.name, err.message);}});}"
  }
 ]
}

Thanks for the help.

CmpCtrl avatar Dec 15 '21 17:12 CmpCtrl

We work quite hard actually to prevent Plotly.js from ever evaluating a string as JS (or HTML) - not 100% successfully just yet but we're in the process of tracking down and removing the last couple instances of this now, which will allow full-featured use of Plotly.js in stricter CSP environments. So we would NOT accept a PR to add this to Plotly.js in the obvious way.

One thing we could imagine doing is allowing a string for click that's simply the name of a function that already exists on the window global namespace. Then all you'd need to do is figure out how to get Plotly.py to load an extra script file in its HTML, put your function in that file function myCopyImage(gd) { Plotly.toImage(gd, ... }, and then reference it as modeBarButtonsToAdd: [{ ..., click: 'myCopyImage' }]

That said Plotly.py has different constraints, so at that layer we can probably do this directly. I wouldn't take the comments in https://github.com/plotly/plotly.py/issues/2114 to mean we wouldn't accept a PR to do this, just that the feature doesn't exist today.

alexcjohnson avatar Dec 15 '21 22:12 alexcjohnson

@alexcjohnson thank you for the reply. Copy that on the security concern, i'm pretty naive in this regard so i didnt see at first glance why passing a function name would be any different than interpreting a string as .js. Now i realize that in different use cases from my own it could be dangerous.

I do like the idea of being able to pass a function name into the click argument as you suggest.

I took a look at the plotly.py side before posting this and didn't come up with a straightforward way to accomplish this. I'll keep looking for a better way to do it though.

CmpCtrl avatar Dec 16 '21 13:12 CmpCtrl

I'll follow up on the Python side in https://github.com/plotly/plotly.py/issues/2114

nicolaskruchten avatar Dec 16 '21 14:12 nicolaskruchten

I'm now trying to use plotly where i cant easily post process the HTML to add the script directly, so I am once again interested in the solution suggested by @alexcjohnson. Unfortunately, i really don't have the chops with js to submit a pr.

CmpCtrl avatar Jul 27 '22 14:07 CmpCtrl