Skip to content
Open
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
48 changes: 1 addition & 47 deletions Desktop/analysis/analysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<void(Json::Value &)> 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;
Expand All @@ -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();
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion Desktop/analysis/analysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 43 additions & 19 deletions Desktop/html/js/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -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 = ''
Expand Down
25 changes: 19 additions & 6 deletions Desktop/utilities/plotschemehandler.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "plotschemehandler.h"
#include "tempfiles.h"
#include <QDir>
#include <QFileInfo>

PlotSchemeHandler::PlotSchemeHandler(QObject *parent) : QWebEngineUrlSchemeHandler(parent)
{
Expand All @@ -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);
}
Expand All @@ -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);
}
Loading