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