Skip to content

Is there a "Hello World" example for clicking on Plotly surface traces and grabbing x,y,z data to be used in a Python code. #7661

@ProfCrouse

Description

@ProfCrouse

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())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions