diff --git a/Desktop/analysis/analysis.cpp b/Desktop/analysis/analysis.cpp index af892eba6c..23254e7477 100644 --- a/Desktop/analysis/analysis.cpp +++ b/Desktop/analysis/analysis.cpp @@ -23,7 +23,6 @@ #include "analyses.h" #include "tempfiles.h" #include "analysisform.h" -#include "columnencoder.h" #include "utilities/qutils.h" #include "utilities/reporter.h" #include "gui/preferencesmodel.h" @@ -291,9 +290,6 @@ void Analysis::imageEdited(const Json::Value & results) updatePlotSize(_imgOptions["name"].asString(), _imgResults.get("width", -1).asInt(), _imgResults.get("height", -1).asInt(), _results); } - // Convert interactiveJsonData file paths to actual JSON objects for the front-end - _imgResults = loadPlotlyJsonInResults(_imgResults); - setStatus(Analysis::Complete); emit imageEditedSignal(this); @@ -427,47 +423,6 @@ std::string Analysis::statusToString(Status status) } } -Json::Value Analysis::loadPlotlyJsonInResults(Json::Value results) const -{ - auto loadFile = [](const std::string & tempFileRelativePath) - { - QFile plotlyJsonFile(tq(TempFiles::sessionDirName() + "/" + tempFileRelativePath)); - - if(plotlyJsonFile.open(QFile::OpenModeFlag::ReadOnly)) - { - Json::Value plotlyJson; - Json::Reader jsonReader; - - jsonReader.parse(plotlyJsonFile.readAll().toStdString(),plotlyJson, false); - - ColumnEncoder::decodeJson(plotlyJson); - - return plotlyJson; - } - return Json::Value(""); - }; - - - std::function recursiveFixer; - - recursiveFixer = [&loadFile, &recursiveFixer](Json::Value & results) - { - if(results.isObject() && results.isMember("interactiveJsonData") && results["interactiveJsonData"].isString() && QFileInfo::exists(tq(TempFiles::sessionDirName() + "/" + results["interactiveJsonData"].asString()))) - results["interactiveJsonData"] = loadFile(results["interactiveJsonData"].asString()); - - if(results.isObject()) - for(const std::string & member : results.getMemberNames()) - recursiveFixer(results[member]); - else if(results.isArray()) - for(int arrayIndex = 0; arrayIndex < results.size(); arrayIndex++) - recursiveFixer(results[arrayIndex]); - }; - - recursiveFixer(results); - - return results; -} - Json::Value Analysis::asJSON(bool withRSource) const { Json::Value analysisAsJson = Json::objectValue; @@ -479,7 +434,7 @@ Json::Value Analysis::asJSON(bool withRSource) const analysisAsJson["rfile"] = _rfile; analysisAsJson["hasReport"] = _hasReport; analysisAsJson["progress"] = _progress; - analysisAsJson["results"] = loadPlotlyJsonInResults(_results); + analysisAsJson["results"] = _results; analysisAsJson["status"] = statusToString(_status); analysisAsJson["options"] = boundValues(); analysisAsJson["userdata"] = userData(); @@ -501,7 +456,6 @@ Json::Value Analysis::asJSON(bool withRSource) const return analysisAsJson; } - void Analysis::checkDefaultTitleFromJASPFile(const Json::Value & analysisData) { //Lets make sure the title changes if the default changed compared with last time. (and the user didnt change it manually of course) diff --git a/Desktop/analysis/analysis.h b/Desktop/analysis/analysis.h index 616fdc53d1..d5b966defb 100644 --- a/Desktop/analysis/analysis.h +++ b/Desktop/analysis/analysis.h @@ -217,7 +217,6 @@ public slots: void initAnalysis(); void setAnalysisForm(AnalysisForm * analysisForm); bool readyToCreateForm() const; - Json::Value loadPlotlyJsonInResults(Json::Value results) const; protected: Status _status = Empty; diff --git a/Desktop/html/js/image.js b/Desktop/html/js/image.js index e0ed872196..ab78db4f9e 100644 --- a/Desktop/html/js/image.js +++ b/Desktop/html/js/image.js @@ -45,10 +45,11 @@ JASPWidgets.imageView = JASPWidgets.objectView.extend({ isEditable: function() { return this.model.get("error") === null; }, isConvertible: function() { return this.model.get("error") === null && this.model.get("convertible") === true; }, hasCollapse: function() { return this.$el.hasClass('jasp-collection-item') === false; }, - hasInteractive: function() { + hasInteractive: function() { if(!useInteractivePlots) return false; - return this.model.get("interactiveJsonData") !== null && this.model.get("interactiveJsonData") !== undefined; }, + const interactiveJsonData = this.model.get("interactiveJsonData"); + return interactiveJsonData !== null && interactiveJsonData !== undefined && interactiveJsonData !== ""; }, saveImageClicked: function() { this.model.trigger("SaveImage:clicked", { data: this.model.get("data"), width: this.model.get("width"), height: this.model.get("height"), name: this.model.get("name") }); }, editImageClicked: function() { this.model.trigger("EditImage:clicked", this.myView, { data: this.model.get("data"), width: this.model.get("width"), height: this.model.get("height"), name: this.model.get("name"), title: this.model.get("title"), type: "interactive" }); }, interactiveImageClicked: function() { @@ -317,29 +318,30 @@ JASPWidgets.imagePrimitive = JASPWidgets.View.extend({ console.log("Plotly render attempt - ID:", this.plotlyId, "Element found:", !!targetEl, "Visible:", targetEl ? $(targetEl).is(':visible') : false, "Retry count:", this.plotlyRetryCount || 0); if (targetEl && $(targetEl).is(':visible')) { - const payload = this.model.get("interactiveJsonData"); - console.log("Rendering Plotly with payload:", payload); - - // Clear any existing plot first - Plotly.purge(targetEl); - targetEl._plotlyInitialized = false; - - // Then create new plot - Plotly.newPlot(targetEl, payload.data, payload.layout) - .then(() => { - console.log("Plotly chart rendered successfully"); - // Mark the element as having a valid Plotly chart - targetEl._plotlyInitialized = true; + this.loadInteractiveJsonData() + .then((payload) => { + console.log("Rendering Plotly with payload:", payload); + + // Clear any existing plot first + Plotly.purge(targetEl); + targetEl._plotlyInitialized = false; + + // Then create new plot + return Plotly.newPlot(targetEl, payload.data, payload.layout) + .then(() => { + console.log("Plotly chart rendered successfully"); + // Mark the element as having a valid Plotly chart + targetEl._plotlyInitialized = true; + + if (payload.hasRangeFrame) + this.addPlotlyRangeRameHooks(targetEl); + }); }) .catch((err) => { console.error("Plotly rendering failed:", err); targetEl._plotlyInitialized = false; }); - if (payload.hasRangeFrame) - this.addPlotlyRangeRameHooks(targetEl); - - } else { // Limit retries to prevent infinite loops this.plotlyRetryCount = (this.plotlyRetryCount || 0) + 1; @@ -352,6 +354,28 @@ JASPWidgets.imagePrimitive = JASPWidgets.View.extend({ } }, + loadInteractiveJsonData: function () { + const interactiveJsonData = this.model.get("interactiveJsonData"); + + if (typeof interactiveJsonData !== "string") + return Promise.resolve(interactiveJsonData); + + const revision = this.model.get("revision"); + const url = (insideJASP ? "plot://" + interactiveJsonData : interactiveJsonData) + "?rev=" + revision; + + return fetch(url) + .then((response) => { + if (!response.ok) + throw new Error("Could not load interactive plot data from " + url); + + return response.json(); + }) + .then((payload) => { + this.model.set("interactiveJsonData", payload); + return payload; + }); + }, + renderDefault: function () { var html = '' diff --git a/Desktop/utilities/plotschemehandler.cpp b/Desktop/utilities/plotschemehandler.cpp index f14bbefd48..b45b2712ee 100644 --- a/Desktop/utilities/plotschemehandler.cpp +++ b/Desktop/utilities/plotschemehandler.cpp @@ -1,5 +1,7 @@ #include "plotschemehandler.h" #include "tempfiles.h" +#include +#include PlotSchemeHandler::PlotSchemeHandler(QObject *parent) : QWebEngineUrlSchemeHandler(parent) { @@ -9,7 +11,7 @@ PlotSchemeHandler::PlotSchemeHandler(QObject *parent) : QWebEngineUrlSchemeHandl void PlotSchemeHandler::createUrlScheme() { QWebEngineUrlScheme plotScheme = QWebEngineUrlScheme("plot"); - plotScheme.setFlags(QWebEngineUrlScheme::ContentSecurityPolicyIgnored); + plotScheme.setFlags(QWebEngineUrlScheme::ContentSecurityPolicyIgnored | QWebEngineUrlScheme::CorsEnabled | QWebEngineUrlScheme::FetchApiAllowed); plotScheme.setSyntax(QWebEngineUrlScheme::Syntax::Path); QWebEngineUrlScheme::registerScheme(plotScheme); } @@ -20,19 +22,30 @@ void PlotSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) QString filePath = QString::fromStdString(TempFiles::sessionDirName()) + fileUrl.toString(QUrl::RemoveScheme | QUrl::RemoveQuery); //Maybe we could remove the whole ?rev=number thing because we are not caching anything here. But maybe webengine does, Im leaving it for now to avoid too many changes. - if(filePath.indexOf(".png") == -1) + QString contentType; + if(filePath.endsWith(".png", Qt::CaseInsensitive)) + contentType = "image/png"; + else if(filePath.endsWith(".json", Qt::CaseInsensitive)) + contentType = "application/json"; + else { request->fail(QWebEngineUrlRequestJob::Error::UrlInvalid); return; } - QFile * png = new QFile(filePath, request); - if(!png->exists()) + QFileInfo sessionDir(QString::fromStdString(TempFiles::sessionDirName())); + QFileInfo requestedFile(filePath); + const QString resourcesPath = QDir::cleanPath(sessionDir.canonicalFilePath() + QDir::separator() + "resources"); + const QString requestedPath = QDir::cleanPath(requestedFile.canonicalFilePath()); + + if(requestedPath.isEmpty() || resourcesPath.isEmpty() || !requestedPath.startsWith(resourcesPath + QDir::separator())) { request->fail(QWebEngineUrlRequestJob::Error::UrlNotFound); return; } - png->open(QIODevice::ReadOnly); - request->reply("image/png", png); + QFile * file = new QFile(requestedPath, request); + file->open(QIODevice::ReadOnly); + + request->reply(contentType.toUtf8(), file); }