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

Feature/ Interactive Chart Save As #4771

Merged
merged 7 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions openbb_terminal/core/plots/plotly.html
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,23 @@

<!-- Title Popup -->
<div class="popup_content" id="popup_title"></div>

<!-- Download Popup -->
<div class="popup_content" id="popup_download">
<p>File saved to:</p>
<p id="download_path"></p>
<div style="float: right; margin-top: 20px">
<button class="_btn" id="openfile_button">Open File</button>
<button class="_btn" id="openfolder_button">Open Folder</button>
<button
class="_btn-tertiary"
id="close_button"
onclick="closePopup()"
>
Close
</button>
</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -209,17 +226,20 @@
!["svg", "pdf"].includes(extension)
) {
if (annotation.text[0] == "/") {
cmd_src = annotation.text;
globals.cmd_src = annotation.text;
globals.cmd_idx =
window.plotly_figure.layout.annotations.indexOf(annotation);
annotation.text = "";

let margin = window.plotly_figure.layout.margin;
globals.old_margin = { ...margin };
if (margin.t != undefined && margin.t > 40) {
margin.t = 40;
}
margin.l = 70;
if (cmd_src == "/stocks/candle") {
margin.r -= 40;
}
window.plotly_figure.layout.margin = margin;
}
}
});
Expand All @@ -235,6 +255,8 @@
let title =
window.plotly_figure?.layout?.title?.text ?? "Interactive Chart";

globals.title = title;

if (title.length > 50) {
document.getElementById("title").style.fontSize = "12px";
}
Expand All @@ -248,12 +270,14 @@
CSV_DIV = document.getElementById("popup_csv");
TEXT_DIV = document.getElementById("popup_text");
TITLE_DIV = document.getElementById("popup_title");
DOWNLOAD_DIV = document.getElementById("popup_download");
OpenBBMain(
window.plotly_figure,
CHART_DIV,
CSV_DIV,
TEXT_DIV,
TITLE_DIV
TITLE_DIV,
DOWNLOAD_DIV
);
}
}, 20);
Expand Down
121 changes: 98 additions & 23 deletions openbb_terminal/core/plots/web/bar_menus.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,49 @@ function uploadImage() {
});
}

function downloadImage(filename, extension) {
const openbb_watermark = {
yref: "paper",
xref: "paper",
x: 1,
y: 0,
text: "OpenBB Terminal",
font_size: 17,
font_color: "gray",
opacity: 0.5,
xanchor: "right",
yanchor: "bottom",
yshift: -80,
xshift: 40,
};

async function setWatermarks(margin, old_index, init = false) {
if (init) {
globals.CHART_DIV.layout.annotations.push(openbb_watermark);
if (globals.cmd_idx && globals.cmd_src) {
globals.CHART_DIV.layout.annotations[globals.cmd_idx].text =
globals.cmd_src;
}

Plotly.relayout(globals.CHART_DIV, {
"title.text": globals.title,
margin: globals.old_margin,
});
}

if (!init) {
if (globals.cmd_idx && globals.cmd_src) {
globals.CHART_DIV.layout.annotations[globals.cmd_idx].text = "";
}
globals.CHART_DIV.layout.annotations.splice(old_index, 1);

Plotly.relayout(globals.CHART_DIV, {
"title.text": "",
margin: margin,
});
}
}

async function downloadImage(filename, extension, writable = undefined) {
const loader = document.getElementById("loader");
loader.classList.add("show");

Expand All @@ -247,20 +289,47 @@ function downloadImage(filename, extension) {
// } else if (extension == "svg") {
// imageDownload = domtoimage.toSvg;
} else if (["svg", "pdf"].includes(extension)) {
Plotly.downloadImage(globals.CHART_DIV, {
format: "svg",
height: globals.CHART_DIV.clientHeight,
width: globals.CHART_DIV.clientWidth,
filename: filename,
});
const margin = globals.CHART_DIV.layout.margin;
const old_index = globals.CHART_DIV.layout.annotations.length;

await setWatermarks(margin, old_index, true);

if (window.showSaveFilePicker && writable) {
await Plotly.toImage(globals.CHART_DIV, {
format: "svg",
height: globals.CHART_DIV.clientHeight,
width: globals.CHART_DIV.clientWidth,
}).then(async function (dataUrl) {
const blob = await fetch(dataUrl).then((r) => r.blob());
await writable.write(blob);
await writable.close();
});
} else {
Plotly.downloadImage(globals.CHART_DIV, {
format: "svg",
height: globals.CHART_DIV.clientHeight,
width: globals.CHART_DIV.clientWidth,
filename: filename,
});
}

await setWatermarks(margin, old_index, false);

return;
} else {
console.log("Invalid extension");
return;
}

imageDownload(document.getElementById("openbb_container"))
.then(function (dataUrl) {
downloadURI(dataUrl, filename + "." + extension);
.then(async function (dataUrl) {
if (window.showSaveFilePicker && writable) {
const blob = await fetch(dataUrl).then((r) => r.blob());
await writable.write(blob);
await writable.close();
} else {
downloadURI(dataUrl, filename + "." + extension);
}
loader.classList.remove("show");
})
.catch(function (error) {
Expand All @@ -279,7 +348,7 @@ function downloadURI(uri, name) {
document.body.removeChild(link);
}

function downloadData(gd) {
async function downloadData(gd, filename, writable = undefined) {
let data = gd.data;
let candlestick = false;
let csv = undefined;
Expand Down Expand Up @@ -363,23 +432,29 @@ function downloadData(gd) {
}
}

let filename = globals.filename;
let blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
if (navigator.msSaveBlob) {
// IE 10+
navigator.msSaveBlob(blob, filename);
} else {
let link = window.document.createElement("a");
if (link.download !== undefined) {
// feature detection
// Browsers that support HTML5 download attribute
let url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = "hidden";
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
// feature detection
// Browsers that support showSaveFilePicker or HTML5 download attribute
if (window.showSaveFilePicker && writable) {
await writable.write(blob);
await writable.close();
} else {
let link = window.document.createElement("a");
if (link.download !== undefined) {
// feature detection
// Browsers that support HTML5 download attribute
let url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = "hidden";
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
}
}
}
85 changes: 73 additions & 12 deletions openbb_terminal/core/plots/web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@ const non_blocking = (func, delay) => {
};
};

function OpenBBMain(plotly_figure, chartdiv, csvdiv, textdiv, titlediv) {
function OpenBBMain(
plotly_figure,
chartdiv,
csvdiv,
textdiv,
titlediv,
downloaddiv
) {
// Main function that plots the graphs and initializes the bar menus
globals.CHART_DIV = chartdiv;
globals.TITLE_DIV = titlediv;
globals.TEXT_DIV = textdiv;
globals.CSV_DIV = csvdiv;
globals.DOWNLOAD_DIV = downloaddiv;
console.log("main.js loaded");
console.log("plotly_figure", plotly_figure);
let graphs = plotly_figure;
Expand Down Expand Up @@ -82,15 +90,15 @@ function OpenBBMain(plotly_figure, chartdiv, csvdiv, textdiv, titlediv) {
{
name: "Download CSV (Ctrl+Shift+S)",
icon: ICONS.downloadCsv,
click: function (gd) {
downloadCsvButton(gd);
click: async function (gd) {
await downloadCsvButton(gd);
},
},
{
name: "Download PNG (Ctrl+S)",
icon: ICONS.downloadImage,
click: function () {
downloadImageButton();
click: async function () {
await downloadImageButton();
},
},
// {
Expand Down Expand Up @@ -349,11 +357,44 @@ function OpenBBMain(plotly_figure, chartdiv, csvdiv, textdiv, titlediv) {
button_pressed(title, active);
}

function downloadImageButton() {
loadingOverlay("Saving PNG");
async function downloadImageButton() {
let filename = globals.filename;
let ext = "png";
let writable;
if (window.showSaveFilePicker) {
const handle = await showSaveFilePicker({
suggestedName: `${filename}.${ext}`,
types: [
{
description: "PNG Image",
accept: {
"image/png": [".png"],
},
},
{
description: "JPEG Image",
accept: {
"image/jpeg": [".jpeg"],
},
},
{
description: "SVG Image",
accept: {
"image/svg+xml": [".svg"],
},
},
],
excludeAcceptAllOption: true,
});
writable = await handle.createWritable();
filename = handle.name;
ext = handle.name.split(".").pop();
}

loadingOverlay(`Saving ${ext.toUpperCase()}`);
hideModebar();
non_blocking(function () {
downloadImage(globals.filename, "png");
non_blocking(async function () {
await downloadImage(filename, ext, writable);
setTimeout(function () {
setTimeout(function () {
loading.classList.remove("show");
Expand All @@ -363,10 +404,30 @@ function OpenBBMain(plotly_figure, chartdiv, csvdiv, textdiv, titlediv) {
}, 2)();
}

function downloadCsvButton(gd) {
async function downloadCsvButton(gd) {
let filename = globals.filename;
let writable;

if (window.showSaveFilePicker) {
const handle = await showSaveFilePicker({
suggestedName: `${filename}.csv`,
types: [
{
description: "CSV File",
accept: {
"image/csv": [".csv"],
},
},
],
excludeAcceptAllOption: true,
});
writable = await handle.createWritable();
filename = handle.name;
}

loadingOverlay("Saving CSV");
setTimeout(function () {
downloadData(gd);
setTimeout(async function () {
await downloadData(gd, filename, writable);
}, 500);
setTimeout(function () {
loading.classList.remove("show");
Expand Down
28 changes: 27 additions & 1 deletion openbb_terminal/core/plots/web/popups.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,35 @@ function get_popup(data = null, popup_id = null) {
`;
popup = globals.TEXT_DIV;
console.log("upload");
} else if (popup_id == "download") {
popup = globals.DOWNLOAD_DIV;

popup.querySelector("#download_path").innerHTML = `
<textarea style="${style} width: 100%; max-width: 100%; max-height: 200px;
margin-top: 8px;" rows="4" cols="50" value="${data}"
placeholder="Enter text here">${data}</textarea><br>
`;

openfile_button = popup.querySelector("#openfile_button");
openfile_button.onclick = function () {
window.ipc.postMessage(`#OPEN:${data}`);
};

openfolder_button = popup.querySelector("#openfolder_button");
openfolder_button.onclick = function () {
folder_path = data.split("\\").slice(0, -1).join("\\");
window.ipc.postMessage(`#OPEN:${folder_path}`);
};

globals.DOWNLOAD_DIV.style.display = "inline-block";
}

let popup_divs = [globals.TITLE_DIV, globals.TEXT_DIV, globals.CSV_DIV];
let popup_divs = [
globals.TITLE_DIV,
globals.TEXT_DIV,
globals.CSV_DIV,
globals.DOWNLOAD_DIV,
];
popup_divs.forEach(function (div) {
if (div.id != popup.id) {
div.style.display = "none";
Expand Down