PyQt6 WebEngineView doesn't load embedded Bokeh plot / local Bokeh server

149 Views Asked by At

I am writing a PyQt6 app that allows the user to select a .csv, and choose a column to be plot. A local bokeh server is created when the app starts. The plot is embedded in a PyQt6 WebEngineView that opens up in a new window.

Unfortunately the url of the plot does not finish loading in the WebEngineView.

I can load "http://google.com" fine, but the local bokeh url "http://localhost:6005/base" doesn't ever send the "loadFinished" signal.

I also get "doh set to "" -- SystemOnly" in the terminal earlier in the code. Don't know if this is an issue.

Using show(p), the plot loads fine in a web browser.

Am I looking in the wrong place for my issue? Is this even possible to do? Should I be using components instead (I want 100% offline application).

import sys

import pandas as pd
from bokeh.io import show
from bokeh.models import ColumnDataSource
from bokeh.plotting import curdoc, figure
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
    QApplication,
    QFileDialog,
    QInputDialog,
    QLabel,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class CSVPlotterApp(QMainWindow):
    def __init__(self):
        super().__init__()

        self.init_ui()

        self.bokeh_server = self.create_bokeh_server()
        
        # Create a BokehPlotWindow instance
        self.bokeh_plot_window = BokehPlotWindow()

    def init_ui(self):
        self.setWindowTitle("CSV Plotter")
        self.setGeometry(100, 100, 800, 600)

        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        self.layout = QVBoxLayout()

        self.label = QLabel("Choose a CSV file and column to plot:")
        self.layout.addWidget(self.label)

        self.file_button = QPushButton("Choose CSV File", self)
        self.file_button.clicked.connect(self.choose_file)
        self.layout.addWidget(self.file_button)

        self.plot_button = QPushButton("Plot", self)
        self.plot_button.clicked.connect(self.plot_data)
        self.layout.addWidget(self.plot_button)

        self.central_widget.setLayout(self.layout)

        self.file_path = None
        self.column_name = None

    def choose_file(self):
        options = QFileDialog.Option(0)
        options |= QFileDialog.Option.ReadOnly

        file_dialog = QFileDialog()
        file_dialog.setOptions(options)
        file_dialog.setNameFilter("CSV Files (*.csv)")
        file_dialog.setDefaultSuffix("csv")

        if file_dialog.exec() == QFileDialog.DialogCode.Accepted:
            self.file_path = file_dialog.selectedFiles()[0]
            self.label.setText(f"Selected CSV file: {self.file_path}")

            # Read the CSV file to get column names
            try:
                df = pd.read_csv(self.file_path)
                column_names = df.columns.tolist()
                column_names_str = "\n".join(column_names)
                column_names_message = f"Column Names:\n{column_names_str}"

                # Display the column names
                QMessageBox.information(self, "Column Names", column_names_message)
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Error reading CSV file: {str(e)}")

    def plot_data(self):
        from PyQt6.QtCore import QUrl
        
        if not self.file_path:
            self.label.setText("Please choose a CSV file first.")
            return

        column_name, ok = QInputDialog.getText(self, "Enter Column", "Enter column name:")
        if not ok:
            return

        self.column_name = column_name

        try:
            df = pd.read_csv(self.file_path)

            # Ensure the column exists in the DataFrame
            if self.column_name not in df.columns:
                self.label.setText(f"Column '{self.column_name}' not found in the CSV file.")
                return

            self.modify_doc(df, column_name)
            
            # Open the BokehPlotWindow and load the Bokeh server URL
            url_app = QUrl(self.url)
            # url_google = QUrl("http://google.com")
            # self.bokeh_plot_window.web_view.setUrl(url_google)
            self.bokeh_plot_window.web_view.setUrl(url_app)
            self.bokeh_plot_window.show()
            
            print(self.bokeh_plot_window.web_view.url().toString())
            print(self.bokeh_plot_window.web_view.loadFinished)
            
            self.bokeh_plot_window.web_view.loadStarted.connect(lambda: self.test("loadStarted"))
            self.bokeh_plot_window.web_view.loadProgress.connect(lambda: self.test("loadProgress"))
            self.bokeh_plot_window.web_view.loadFinished.connect(lambda: self.test("loadFinished"))
            
        except Exception as e:
            import traceback
            traceback.print_exc()
            self.label.setText(f"Error plotting data: {str(e)}")
    
    def test(self, event_type):
        print(f"All Good - Event Type: {event_type}")

    def create_bokeh_server(self):
        # Function to create a Bokeh server instance
        from bokeh.application import Application
        from bokeh.application.handlers.function import FunctionHandler
        from bokeh.server.server import Server

        # Define your Bokeh app function
        def create_empty_bokeh_server(doc):
            # Call the modify_doc method with None values to trigger initial setup
            self.modify_doc(None, None)

        # Create the Bokeh application handler
        handler = FunctionHandler(create_empty_bokeh_server)
        bokeh_application = Application(handler)

        # Start the Bokeh server with the Application
        bokeh_server = Server({"/base": bokeh_application}, port=6005)
        bokeh_server.start()

        # Print the server URL
        self.url = f"http://localhost:{bokeh_server.port}/base"
        print(f"Bokeh server running at: {self.url}")

        return bokeh_server

    def modify_doc(self, df, column_name):
        # This function is called to modify the Bokeh document with the plot
        if df is None or column_name is None:
            return  # Skip modification if df or column_name is None

        plot_title = f"Plot of {self.column_name} from {self.file_path}"

        source = ColumnDataSource(df)
        x_values = [str(val) for val in df.index]

        p = figure(
            title=plot_title,
            x_range=x_values,
            height=400,
            width=600,
            toolbar_location=None,
            tools="",
        )
        p.line(x="index", y=self.column_name, source=source, line_width=2)
        
        # show(p)
        
        # Get the Bokeh document using curdoc()
        doc = curdoc()
        print(doc._roots)
        # Add the plot to the Bokeh document
        doc.add_root(p)
        print(doc._roots)


class BokehPlotWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("Bokeh Plot Window")
        self.setGeometry(100, 100, 800, 600)

        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        self.layout = QVBoxLayout()

        # self.label = QLabel("Bokeh Plot:")
        # self.layout.addWidget(self.label)

        self.web_view = QWebEngineView()
        self.layout.addWidget(self.web_view)

        self.central_widget.setLayout(self.layout)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = CSVPlotterApp()
    window.show()
    sys.exit(app.exec())
0

There are 0 best solutions below