From 42c9601d8456fe73c82ecad8afd4897a130868e6 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:18:27 -0400 Subject: [PATCH 1/7] add button placeholder which shows chart JSON --- src/components/modebar/buttons.js | 10 ++++++++++ src/components/modebar/manage.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index a67fd18b0f0..413c0824b46 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -85,6 +85,16 @@ modeBarButtons.editInChartStudio = { } }; +modeBarButtons.uploadToCloud = { + name: 'uploadToCloud', + title: function(gd) { return _(gd, 'Upload to Cloud'); }, + icon: Icons.disk, + click: function(gd) { + var fig = Plots.graphJson(gd, false, 'keepdata', 'object', true, true); + alert(JSON.stringify(fig, null, 2)); + } +}; + modeBarButtons.zoom2d = { name: 'zoom2d', _cat: 'zoom', diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 3c9a4626192..d6b485be428 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -144,7 +144,7 @@ function getButtonGroups(gd) { } // buttons common to all plot types - var commonGroup = ['toImage']; + var commonGroup = ['toImage', 'uploadToCloud']; if(context.showEditInChartStudio) commonGroup.push('editInChartStudio'); else if(context.showSendToCloud) commonGroup.push('sendDataToCloud'); addGroup(commonGroup); From 8ce55121a3f1c80726c65d6daf38bb250bb93e59 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Mon, 11 May 2026 14:00:13 -0400 Subject: [PATCH 2/7] add icon --- src/components/modebar/buttons.js | 8 +++----- src/components/modebar/cloud.js | 11 +++++++++++ src/fonts/ploticon.js | 6 ++++++ 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 src/components/modebar/cloud.js diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 413c0824b46..c6b44f088b8 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -4,6 +4,7 @@ var Registry = require('../../registry'); var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); +var uploadToCloud = require('./cloud').uploadToCloud; var eraseActiveShape = require('../shapes/draw').eraseActiveShape; var Lib = require('../../lib'); var _ = Lib._; @@ -88,11 +89,8 @@ modeBarButtons.editInChartStudio = { modeBarButtons.uploadToCloud = { name: 'uploadToCloud', title: function(gd) { return _(gd, 'Upload to Cloud'); }, - icon: Icons.disk, - click: function(gd) { - var fig = Plots.graphJson(gd, false, 'keepdata', 'object', true, true); - alert(JSON.stringify(fig, null, 2)); - } + icon: Icons.cloudupload, + click: uploadToCloud, }; modeBarButtons.zoom2d = { diff --git a/src/components/modebar/cloud.js b/src/components/modebar/cloud.js new file mode 100644 index 00000000000..915c26904ff --- /dev/null +++ b/src/components/modebar/cloud.js @@ -0,0 +1,11 @@ +var Plots = require('../../plots/plots'); + + +function uploadToCloud(gd) { + var fig = Plots.graphJson(gd, false, 'keepdata', 'object', true, true); + alert(JSON.stringify(fig, null, 2)); +} + +module.exports = { + uploadToCloud: uploadToCloud +}; \ No newline at end of file diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index f9f496f7ed0..53fdbedbe40 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -103,6 +103,12 @@ module.exports = { path: 'm214-7h429v214h-429v-214z m500 0h72v500q0 8-6 21t-11 20l-157 156q-5 6-19 12t-22 5v-232q0-22-15-38t-38-16h-322q-22 0-37 16t-16 38v232h-72v-714h72v232q0 22 16 38t37 16h465q22 0 38-16t15-38v-232z m-214 518v178q0 8-5 13t-13 5h-107q-7 0-13-5t-5-13v-178q0-8 5-13t13-5h107q7 0 13 5t5 13z m357-18v-518q0-22-15-38t-38-16h-750q-23 0-38 16t-16 38v750q0 22 16 38t38 16h517q23 0 50-12t42-26l156-157q16-15 27-42t11-49z', transform: 'matrix(1 0 0 -1 0 850)' }, + cloudupload: { + width: 640, + height: 640, + path: 'M176 544C96.5 544 32 479.5 32 400C32 336.6 73 282.8 129.9 263.5C128.6 255.8 128 248 128 240C128 160.5 192.5 96 272 96C327.4 96 375.5 127.3 399.6 173.1C413.8 164.8 430.4 160 448 160C501 160 544 203 544 256C544 271.7 540.2 286.6 533.5 299.7C577.5 320 608 364.4 608 416C608 486.7 550.7 544 480 544L176 544zM337 255C327.6 245.6 312.4 245.6 303.1 255L231.1 327C221.7 336.4 221.7 351.6 231.1 360.9C240.5 370.2 255.7 370.3 265 360.9L296 329.9L296 432C296 445.3 306.7 456 320 456C333.3 456 344 445.3 344 432L344 329.9L375 360.9C384.4 370.3 399.6 370.3 408.9 360.9C418.2 351.5 418.3 336.3 408.9 327L336.9 255z', + transform: 'matrix(1 0 0 1 -15 -15)' + }, drawopenpath: { width: 70, height: 70, From ad9ddfcd4c0c12b4f9eb7457700bd2b6b282295f Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 14 May 2026 12:08:58 -0400 Subject: [PATCH 3/7] use existing sendChartToCloud function --- src/components/modebar/buttons.js | 25 ++++--------------------- src/components/modebar/cloud.js | 11 ----------- src/components/modebar/manage.js | 5 ++--- src/plot_api/plot_config.js | 30 +++++++----------------------- src/plots/plots.js | 10 +++++++--- 5 files changed, 20 insertions(+), 61 deletions(-) delete mode 100644 src/components/modebar/cloud.js diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index c6b44f088b8..3b14de06760 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -4,7 +4,6 @@ var Registry = require('../../registry'); var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); -var uploadToCloud = require('./cloud').uploadToCloud; var eraseActiveShape = require('../shapes/draw').eraseActiveShape; var Lib = require('../../lib'); var _ = Lib._; @@ -68,31 +67,15 @@ modeBarButtons.toImage = { } }; -modeBarButtons.sendDataToCloud = { - name: 'sendDataToCloud', - title: function(gd) { return _(gd, 'Edit in Chart Studio'); }, - icon: Icons.disk, - click: function(gd) { - Plots.sendDataToCloud(gd); - } -}; - -modeBarButtons.editInChartStudio = { - name: 'editInChartStudio', - title: function(gd) { return _(gd, 'Edit in Chart Studio'); }, - icon: Icons.pencil, +modeBarButtons.sendChartToCloud = { + name: 'sendChartToCloud', + title: function(gd) { return _(gd, 'Share with Plotly Cloud'); }, + icon: Icons.cloudupload, click: function(gd) { Plots.sendDataToCloud(gd); } }; -modeBarButtons.uploadToCloud = { - name: 'uploadToCloud', - title: function(gd) { return _(gd, 'Upload to Cloud'); }, - icon: Icons.cloudupload, - click: uploadToCloud, -}; - modeBarButtons.zoom2d = { name: 'zoom2d', _cat: 'zoom', diff --git a/src/components/modebar/cloud.js b/src/components/modebar/cloud.js deleted file mode 100644 index 915c26904ff..00000000000 --- a/src/components/modebar/cloud.js +++ /dev/null @@ -1,11 +0,0 @@ -var Plots = require('../../plots/plots'); - - -function uploadToCloud(gd) { - var fig = Plots.graphJson(gd, false, 'keepdata', 'object', true, true); - alert(JSON.stringify(fig, null, 2)); -} - -module.exports = { - uploadToCloud: uploadToCloud -}; \ No newline at end of file diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index d6b485be428..56713ca22db 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -144,9 +144,8 @@ function getButtonGroups(gd) { } // buttons common to all plot types - var commonGroup = ['toImage', 'uploadToCloud']; - if(context.showEditInChartStudio) commonGroup.push('editInChartStudio'); - else if(context.showSendToCloud) commonGroup.push('sendDataToCloud'); + var commonGroup = ['toImage']; + if(context.showSendToCloud) commonGroup.push('sendChartToCloud'); addGroup(commonGroup); var zoomGroup = []; diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 09a22f99e55..4478a0ef695 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -35,12 +35,8 @@ var configAttributes = { valType: 'string', dflt: '', description: [ - 'When set it determines base URL for', - 'the \'Edit in Chart Studio\' `showEditInChartStudio`/`showSendToCloud` mode bar button', - 'and the showLink/sendData on-graph link.', - 'To enable sending your data to Chart Studio Cloud, you need to', - 'set both `plotlyServerURL` to \'https://chart-studio.plotly.com\' and', - 'also set `showSendToCloud` to true.' + 'Sets the URL for the `sendChartToCloud` modebar button.', + 'When clicked, the button will send the chart data to this URL.', ].join(' ') }, @@ -275,24 +271,12 @@ var configAttributes = { }, showSendToCloud: { valType: 'boolean', - dflt: false, - description: [ - 'Should we include a ModeBar button, labeled "Edit in Chart Studio",', - 'that sends this chart to chart-studio.plotly.com (formerly plot.ly) or another plotly server', - 'as specified by `plotlyServerURL` for editing, export, etc? Prior to version 1.43.0', - 'this button was included by default, now it is opt-in using this flag.', - 'Note that this button can (depending on `plotlyServerURL` being set) send your data', - 'to an external server. However that server does not persist your data', - 'until you arrive at the Chart Studio and explicitly click "Save".' - ].join(' ') - }, - showEditInChartStudio: { - valType: 'boolean', - dflt: false, + dflt: true, description: [ - 'Same as `showSendToCloud`, but use a pencil icon instead of a floppy-disk.', - 'Note that if both `showSendToCloud` and `showEditInChartStudio` are turned,', - 'only `showEditInChartStudio` will be honored.' + 'Should we include a modebar button that sends this chart to a URL', + 'specified by `plotlyServerURL`, for sharing the chart with others?', + 'Note that this button can (depending on `plotlyServerURL` being set)', + 'send your data to an external server.' ].join(' ') }, modeBarButtonsToRemove: { diff --git a/src/plots/plots.js b/src/plots/plots.js index b3ef101ef33..cf659cf18e3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -201,8 +201,11 @@ function positionPlayWithData(gd, container) { } plots.sendDataToCloud = function(gd) { - var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; - if(!baseUrl) return; + var destUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; + if(!destUrl) { + console.error('No destination URL provided (plotlyServerURL is not set)'); + return; + } gd.emit('plotly_beforeexport'); @@ -214,7 +217,7 @@ plots.sendDataToCloud = function(gd) { var hiddenform = hiddenformDiv .append('form') .attr({ - action: baseUrl + '/external', + action: destUrl, method: 'post', target: '_blank' }); @@ -227,6 +230,7 @@ plots.sendDataToCloud = function(gd) { }); hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); + console.log(`sending chart object to ${destUrl}`); hiddenform.node().submit(); hiddenformDiv.remove(); From 100da7ba21ed9f86e97f819840ed8acef713b627 Mon Sep 17 00:00:00 2001 From: Emily KL <4672118+emilykl@users.noreply.github.com> Date: Thu, 14 May 2026 12:23:21 -0400 Subject: [PATCH 4/7] update plot-schema --- test/plot-schema.json | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/plot-schema.json b/test/plot-schema.json index e0b7c5f1371..18886d37287 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -279,7 +279,7 @@ "valType": "number" }, "plotlyServerURL": { - "description": "When set it determines base URL for the 'Edit in Chart Studio' `showEditInChartStudio`/`showSendToCloud` mode bar button and the showLink/sendData on-graph link. To enable sending your data to Chart Studio Cloud, you need to set both `plotlyServerURL` to 'https://chart-studio.plotly.com' and also set `showSendToCloud` to true.", + "description": "Sets the URL for the `sendChartToCloud` modebar button. When clicked, the button will send the chart data to this URL.", "dflt": "", "valType": "string" }, @@ -330,19 +330,14 @@ "dflt": true, "valType": "boolean" }, - "showEditInChartStudio": { - "description": "Same as `showSendToCloud`, but use a pencil icon instead of a floppy-disk. Note that if both `showSendToCloud` and `showEditInChartStudio` are turned, only `showEditInChartStudio` will be honored.", - "dflt": false, - "valType": "boolean" - }, "showLink": { "description": "Determines whether a link to Chart Studio Cloud is displayed at the bottom right corner of resulting graphs. Use with `sendData` and `linkText`.", "dflt": false, "valType": "boolean" }, "showSendToCloud": { - "description": "Should we include a ModeBar button, labeled \"Edit in Chart Studio\", that sends this chart to chart-studio.plotly.com (formerly plot.ly) or another plotly server as specified by `plotlyServerURL` for editing, export, etc? Prior to version 1.43.0 this button was included by default, now it is opt-in using this flag. Note that this button can (depending on `plotlyServerURL` being set) send your data to an external server. However that server does not persist your data until you arrive at the Chart Studio and explicitly click \"Save\".", - "dflt": false, + "description": "Should we include a modebar button that sends this chart to a URL specified by `plotlyServerURL`, for sharing the chart with others? Note that this button can (depending on `plotlyServerURL` being set) send your data to an external server.", + "dflt": true, "valType": "boolean" }, "showSources": { @@ -4628,7 +4623,7 @@ }, "remove": { "arrayOk": true, - "description": "Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *editInChartStudio*, *editinchartstudio*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViewMapbox*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendDataToCloud*, *senddatatocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *togglehover*, *togglespikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomInMapbox*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomOutMapbox*, *zoomin*, *zoomout*.", + "description": "Determines which predefined modebar buttons to remove. Similar to `config.modeBarButtonsToRemove` option. This may include *autoScale2d*, *autoscale*, *hoverCompareCartesian*, *hovercompare*, *lasso*, *lasso2d*, *orbitRotation*, *orbitrotation*, *pan*, *pan2d*, *pan3d*, *reset*, *resetCameraDefault3d*, *resetCameraLastSave3d*, *resetGeo*, *resetSankeyGroup*, *resetScale2d*, *resetViewMap*, *resetViewMapbox*, *resetViews*, *resetcameradefault*, *resetcameralastsave*, *resetsankeygroup*, *resetscale*, *resetview*, *resetviews*, *select*, *select2d*, *sendChartToCloud*, *sendcharttocloud*, *tableRotation*, *tablerotation*, *toImage*, *toggleHover*, *toggleSpikelines*, *togglehover*, *togglespikelines*, *toimage*, *zoom*, *zoom2d*, *zoom3d*, *zoomIn2d*, *zoomInGeo*, *zoomInMap*, *zoomInMapbox*, *zoomOut2d*, *zoomOutGeo*, *zoomOutMap*, *zoomOutMapbox*, *zoomin*, *zoomout*.", "dflt": "", "editType": "modebar", "valType": "string" From 82f54373db881fb761ff2f7651d22f547de9730e Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 28 May 2026 13:34:13 -0700 Subject: [PATCH 5/7] Update approach to use postMessage and wait for ready signal before sending data to cloud --- src/plots/plots.js | 66 +++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index cf659cf18e3..5a4f7d187f1 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -6,6 +6,7 @@ var formatLocale = require('d3-format').formatLocale; var isNumeric = require('fast-isnumeric'); var b64encode = require('base64-arraybuffer'); +var version = require('../version').version; var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); var Template = require('../plot_api/plot_template'); @@ -201,40 +202,57 @@ function positionPlayWithData(gd, container) { } plots.sendDataToCloud = function(gd) { - var destUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; - if(!destUrl) { + var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; + if(!baseUrl) { console.error('No destination URL provided (plotlyServerURL is not set)'); return; } + // Plotly Cloud origin, used to validate incoming messages and to target outgoing ones. + var cloudOrigin; + try { + cloudOrigin = new URL(baseUrl).origin; + } catch(e) { + console.error('Invalid plotlyServerURL: ' + baseUrl); + return; + } + + // The page that handles login and signals back when authentication succeeds. + var uploadUrl = baseUrl.replace(/\/+$/, '') + '/upload'; + gd.emit('plotly_beforeexport'); - var hiddenformDiv = d3.select(gd) - .append('div') - .attr('id', 'hiddenform') - .style('display', 'none'); + // Build the request body: the chart JSON plus the plotly.js version used to + // generate it, so Cloud can host the chart with a compatible plotly.js version. + var chart = JSON.parse(plots.graphJson(gd, false, 'keepdata')); + chart.version = version; + + // Open the Cloud login page in a new tab. We keep a reference so we can post + // the chart back to it once Cloud reports that authentication succeeded. + var cloudWindow = window.open(uploadUrl, '_blank'); + if(!cloudWindow) { + console.error('Unable to open Plotly Cloud (the popup may have been blocked)'); + gd.emit('plotly_afterexport'); + return; + } - var hiddenform = hiddenformDiv - .append('form') - .attr({ - action: destUrl, - method: 'post', - target: '_blank' - }); + var handleMessage = function(event) { + // Only trust messages coming from the Cloud origin. + if(event.origin !== cloudOrigin) return; - var hiddenformInput = hiddenform - .append('input') - .attr({ - type: 'text', - name: 'data' - }); + if(event.data && event.data.type === 'authenticated') { + cloudWindow.postMessage({ + type: 'chart', + chart: chart + }, cloudOrigin); + + window.removeEventListener('message', handleMessage); + gd.emit('plotly_afterexport'); + } + }; - hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); - console.log(`sending chart object to ${destUrl}`); - hiddenform.node().submit(); - hiddenformDiv.remove(); + window.addEventListener('message', handleMessage); - gd.emit('plotly_afterexport'); return false; }; From ba91b115add654f24adb40a07dbe2148884730ae Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 28 May 2026 14:12:13 -0700 Subject: [PATCH 6/7] Create a confirmation dialog --- build/plotcss.js | 11 ++++ src/components/modebar/buttons.js | 6 +- src/components/modebar/cloud_confirm.js | 69 ++++++++++++++++++++ src/css/_cloud_dialog.scss | 83 +++++++++++++++++++++++++ src/css/style.scss | 1 + 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/components/modebar/cloud_confirm.js create mode 100644 src/css/_cloud_dialog.scss diff --git a/build/plotcss.js b/build/plotcss.js index 0fa41225bc2..b1e2798597f 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -50,6 +50,17 @@ var rules = { "X [data-title]:after": "content:attr(data-title);background:#69738a;color:#fff;padding:8px 10px;font-size:12px;line-height:12px;white-space:nowrap;margin-right:-18px;border-radius:2px;", "X .vertical [data-title]:before,X .vertical [data-title]:after": "top:0%;right:200%;", "X .vertical [data-title]:before": "border:6px solid rgba(0,0,0,0);border-left-color:#69738a;margin-top:8px;margin-right:-30px;", + "X .plotly-cloud-dialog": "font-family:\"Open Sans\",verdana,arial,sans-serif;position:absolute;top:0;left:0;width:100%;height:100%;z-index:1001;display:flex;align-items:center;justify-content:center;background-color:rgba(0,0,0,.4);", + "X .plotly-cloud-dialog .plotly-cloud-dialog-box": "box-sizing:border-box;min-width:300px;max-width:420px;padding:20px 24px;background-color:#fff;border:1px solid #e0e2e5;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,.25);font-size:13px;color:#2a3f5f;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-title": "font-size:16px;font-weight:bold;margin-bottom:12px;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-message": "line-height:1.5;overflow-wrap:break-word;word-wrap:break-word;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-buttons": "display:flex;justify-content:flex-end;margin-top:20px;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn": "font-family:inherit;font-size:13px;padding:7px 16px;margin-left:8px;border-radius:3px;border:1px solid rgba(0,0,0,0);cursor:pointer;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn:focus-visible": "outline:2px solid #447adb;outline-offset:1px;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel": "background-color:#fff;border-color:#e0e2e5;color:#777;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--cancel:hover": "background-color:#f3f3f3;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--confirm": "background-color:#447adb;color:#fff;", + "X .plotly-cloud-dialog .plotly-cloud-dialog-btn--confirm:hover": "background-color:#1d3b84;", Y: "font-family:\"Open Sans\",verdana,arial,sans-serif;position:fixed;top:50px;right:20px;z-index:10000;font-size:10pt;max-width:180px;", "Y p": "margin:0;", "Y .notifier-note": "min-width:180px;max-width:250px;border:1px solid #fff;z-index:3000;margin:0;background-color:#8c97af;background-color:rgba(140,151,175,.9);color:#fff;padding:10px;overflow-wrap:break-word;word-wrap:break-word;-ms-hyphens:auto;-webkit-hyphens:auto;hyphens:auto;", diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3b14de06760..41580c9d85b 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -5,6 +5,7 @@ var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); var eraseActiveShape = require('../shapes/draw').eraseActiveShape; +var confirmCloudDialog = require('./cloud_confirm'); var Lib = require('../../lib'); var _ = Lib._; @@ -72,7 +73,10 @@ modeBarButtons.sendChartToCloud = { title: function(gd) { return _(gd, 'Share with Plotly Cloud'); }, icon: Icons.cloudupload, click: function(gd) { - Plots.sendDataToCloud(gd); + var serverUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; + confirmCloudDialog(gd, serverUrl, function() { + Plots.sendDataToCloud(gd); + }); } }; diff --git a/src/components/modebar/cloud_confirm.js b/src/components/modebar/cloud_confirm.js new file mode 100644 index 00000000000..f0c34a6ac4e --- /dev/null +++ b/src/components/modebar/cloud_confirm.js @@ -0,0 +1,69 @@ +'use strict'; + +var d3 = require('@plotly/d3'); + +/** + * Show a styled confirmation dialog before sharing a chart with Plotly Cloud. + * + * The dialog is appended to the plot's positioning container (.svg-container) + * so it is centered over the plot rather than the whole viewport. It can be + * dismissed by clicking Cancel, clicking the backdrop, or pressing Escape. + * + * @param {DOM node} gd - the graph div, used to scope the dialog to the plot + * @param {string} serverUrl - destination shown in the dialog message + * @param {function} onConfirm - called when the user confirms the upload + */ +module.exports = function confirmCloudDialog(gd, serverUrl, onConfirm) { + var container = d3.select(gd._fullLayout._paperdiv.node()); + + // Never stack dialogs - drop any that is already open. + container.selectAll('.plotly-cloud-dialog').remove(); + + var overlay = container + .append('div') + .classed('plotly-cloud-dialog', true); + + var dialog = overlay.append('div') + .classed('plotly-cloud-dialog-box', true); + + dialog.append('div') + .classed('plotly-cloud-dialog-title', true) + .text('Share with Plotly Cloud'); + + dialog.append('div') + .classed('plotly-cloud-dialog-message', true) + .text('Your chart data will be sent to ' + serverUrl + '.'); + + var buttons = dialog.append('div') + .classed('plotly-cloud-dialog-buttons', true); + + function close() { + overlay.remove(); + document.removeEventListener('keydown', onKeydown); + } + + function onKeydown(e) { + if(e.key === 'Escape' || e.keyCode === 27) close(); + } + document.addEventListener('keydown', onKeydown); + + // Clicking the backdrop (but not the dialog box) cancels. + overlay.on('click', function() { + if(d3.event.target === overlay.node()) close(); + }); + + buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--cancel', true) + .text('Cancel') + .on('click', close); + + buttons.append('button') + .classed('plotly-cloud-dialog-btn', true) + .classed('plotly-cloud-dialog-btn--confirm', true) + .text('Share') + .on('click', function() { + close(); + onConfirm(); + }); +}; diff --git a/src/css/_cloud_dialog.scss b/src/css/_cloud_dialog.scss new file mode 100644 index 00000000000..edb56c892c2 --- /dev/null +++ b/src/css/_cloud_dialog.scss @@ -0,0 +1,83 @@ +.plotly-cloud-dialog { + font-family: 'Open Sans', verdana, arial, sans-serif; // Because not all contexts have this specified. + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1001; + + display: flex; + align-items: center; + justify-content: center; + + background-color: rgba(0, 0, 0, 0.4); + + .plotly-cloud-dialog-box { + box-sizing: border-box; + min-width: 300px; + max-width: 420px; + padding: 20px 24px; + + background-color: $color-bg-light; + border: 1px solid $color-bg-darker; + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); + + font-size: 13px; + color: #2a3f5f; + } + + .plotly-cloud-dialog-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 12px; + } + + .plotly-cloud-dialog-message { + line-height: 1.5; + overflow-wrap: break-word; + word-wrap: break-word; + } + + .plotly-cloud-dialog-buttons { + display: flex; + justify-content: flex-end; + margin-top: 20px; + } + + .plotly-cloud-dialog-btn { + font-family: inherit; + font-size: 13px; + padding: 7px 16px; + margin-left: 8px; + + border-radius: 3px; + border: 1px solid transparent; + cursor: pointer; + + &:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 1px; + } + } + + .plotly-cloud-dialog-btn--cancel { + background-color: $color-bg-light; + border-color: $color-bg-darker; + color: $color-muted-text; + + &:hover { + background-color: $color-bg-base; + } + } + + .plotly-cloud-dialog-btn--confirm { + background-color: $color-brand-primary; + color: $color-bg-light; + + &:hover { + background-color: $color-brand-accent; + } + } +} diff --git a/src/css/style.scss b/src/css/style.scss index 8305dfe36b4..9b3d544e71d 100644 --- a/src/css/style.scss +++ b/src/css/style.scss @@ -6,5 +6,6 @@ @import "cursor.scss"; @import "modebar.scss"; @import "tooltip.scss"; + @import "cloud_dialog.scss"; } @import "notifier.scss"; From 717fc265d77f5f5b4e6ccde178b5fb7b8f2e3213 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 28 May 2026 14:38:12 -0700 Subject: [PATCH 7/7] Add default URL for plotlyServerURL --- src/plot_api/plot_config.js | 2 +- src/plots/plots.js | 7 +++---- test/plot-schema.json | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 4478a0ef695..98b9ced36c5 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -33,7 +33,7 @@ var configAttributes = { plotlyServerURL: { valType: 'string', - dflt: '', + dflt: 'https://cloud.plotly.com/upload', description: [ 'Sets the URL for the `sendChartToCloud` modebar button.', 'When clicked, the button will send the chart data to this URL.', diff --git a/src/plots/plots.js b/src/plots/plots.js index 5a4f7d187f1..6defbadbbe7 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -209,6 +209,8 @@ plots.sendDataToCloud = function(gd) { } // Plotly Cloud origin, used to validate incoming messages and to target outgoing ones. + // `baseUrl` (plotlyServerURL) is the upload page that handles login and signals + // back when authentication succeeds. var cloudOrigin; try { cloudOrigin = new URL(baseUrl).origin; @@ -217,9 +219,6 @@ plots.sendDataToCloud = function(gd) { return; } - // The page that handles login and signals back when authentication succeeds. - var uploadUrl = baseUrl.replace(/\/+$/, '') + '/upload'; - gd.emit('plotly_beforeexport'); // Build the request body: the chart JSON plus the plotly.js version used to @@ -229,7 +228,7 @@ plots.sendDataToCloud = function(gd) { // Open the Cloud login page in a new tab. We keep a reference so we can post // the chart back to it once Cloud reports that authentication succeeded. - var cloudWindow = window.open(uploadUrl, '_blank'); + var cloudWindow = window.open(baseUrl, '_blank'); if(!cloudWindow) { console.error('Unable to open Plotly Cloud (the popup may have been blocked)'); gd.emit('plotly_afterexport'); diff --git a/test/plot-schema.json b/test/plot-schema.json index 1b515412ed9..06ea3eb1e32 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -280,7 +280,7 @@ }, "plotlyServerURL": { "description": "Sets the URL for the `sendChartToCloud` modebar button. When clicked, the button will send the chart data to this URL.", - "dflt": "", + "dflt": "https://cloud.plotly.com/upload", "valType": "string" }, "queueLength": {