-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
I have learned that it is very difficult to get x,y,z data point values from a Plotly surface plot (3D) that can then be used in your python code. I must imagine that someone has created a "Hello World" example at this point in time to show the community how to do this! Egad. I have tried everything that I can imagine. My attempt is below...but it does not work:
import sys
import json
import numpy as np
import plotly.graph_objects as go
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import QUrl, QObject, pyqtSlot, Qt
import os
from PyQt6.QtCore import QTimer, pyqtSlot
Define a static DIV ID
PLOT_DIV_ID = "my-plotly-graph-container"
1. Python Object to receive data from JavaScript
class CallHandler(QObject):
def init(self, status_label):
super().init()
self.status_label = status_label
@pyqtSlot(str)
def handle_click(self, point_data_json):
"""
Parses JSON data specific to a 3D surface plot click event.
"""
try:
point_data = json.loads(point_data_json)
if point_data and 'points' in point_data and len(point_data['points']) > 0:
# For 3D surface plots, coordinates are generally accessed via data['points'][0].x, etc.
point = point_data['points'][0]
x = point.get('x')
y = point.get('y')
z = point.get('z')
if all(c is not None for c in [x, y, z]):
output_str = f"Clicked point: x={x:.2f}, y={y:.2f}, z={z:.2f}"
print(output_str)
self.status_label.setText(output_str)
else:
self.status_label.setText("Could not extract XYZ coordinates.")
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
self.status_label.setText("Error processing click data.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
self.status_label.setText(f"An error occurred: {e}")
class PlotlyViewer(QMainWindow):
def init(self):
super().init()
self.setWindowTitle("Plotly Surface in PyQt6 (Click to get Data Point)")
self.setGeometry(100, 100, 1000, 700)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
central_widget.setLayout(layout)
self.status_label = QLabel("Loading plot...")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("padding: 10px; font-size: 14px;")
layout.addWidget(self.status_label)
self.browser = QWebEngineView()
layout.addWidget(self.browser)
self.call_handler = CallHandler(self.status_label)
self.channel = QWebChannel()
self.channel.registerObject("callHandler", self.call_handler)
self.browser.page().setWebChannel(self.channel)
self.browser.loadFinished.connect(self.on_page_load_finished)
self.load_plotly_plot()
def load_plotly_plot(self):
x_vals = np.linspace(-5, 5, 50)
y_vals = np.linspace(-5, 5, 50)
x, y = np.meshgrid(x_vals, y_vals)
z = np.sin(np.sqrt(x ** 2 + y ** 2))
fig = go.Figure(data=[go.Surface(z=z, x=x_vals, y=y_vals)])
fig.update_layout(title='Interactive Surface Plot', autosize=True)
# **FIX 1:** Embed Plotly.js directly into the HTML file for reliable local loading
self.html_content = fig.to_html(
full_html=True,
include_plotlyjs=True, # This embeds the JS library locally
div_id=PLOT_DIV_ID
)
# Save the HTML to a temporary file
self.temp_path = os.path.join(os.getcwd(), "temp_plot.html")
with open(self.temp_path, "w", encoding="utf-8") as f:
f.write(self.html_content)
self.browser.setUrl(QUrl.fromLocalFile(self.temp_path))
@pyqtSlot(bool)
def on_page_load_finished(self, success):
"""Injects the JS event listener only after the page and embedded Plotly JS have loaded."""
if success:
self.status_label.setText("Click on the surface plot to get X, Y, Z coordinates.")
# FIX: Use QTimer to delay JS injection slightly, ensuring Plotly JS is ready.
# A 500ms delay is usually sufficient.
QTimer.singleShot(500, self._inject_javascript_listener)
else:
self.status_label.setText("Error loading plot in QWebEngineView.")
def _inject_javascript_listener(self):
"""Helper method to run the JavaScript injection script."""
js_injection_script = f"""
(function() {{
if (window.qt && window.qt.webChannelTransport) {{
new QWebChannel(window.qt.webChannelTransport, function(channel) {{
window.callHandler = channel.objects.callHandler;
var plotElement = document.getElementById('{PLOT_DIV_ID}');
if(plotElement) {{
// Attach the listener now that we are confident the plot is rendered
plotElement.on('plotly_click', function(data){{
if(data.points && data.points.length > 0) {{
window.callHandler.handle_click(JSON.stringify(data));
}}
}});
}} else {{
console.error("Plot div not found!");
}}
}});
}}
}})();
"""
self.browser.page().runJavaScript(js_injection_script)
if name == 'main':
temp_file = os.path.join(os.getcwd(), "temp_plot.html")
def cleanup():
if os.path.exists(temp_file):
os.remove(temp_file)
print(f"Cleaned up {temp_file}")
app = QApplication(sys.argv)
app.aboutToQuit.connect(cleanup)
window = PlotlyViewer()
window.show()
sys.exit(app.exec())