QWebEngineView: "cannot read property 'pageX' of undefined" when muting bokeh legend

348 views Asked by At

I'm using PyQt5 to create a GUI and in this GUI I visualize Bokeh graphs using QWebEngineView.

It works fine but when I tried to implement the "muting" legend like this I get an error:

js: Uncaught TypeError: Cannot read property 'pageX' of undefined

If I use the show method, I get the expecting result in my browser. However if I use save and display it to the QWebEngineView I get the mentioned error.

Any ideas?

The slot in my Gui Class to plot and show in the QWebEngineView:

Notes: Ignore the Bar and Pizza plots, it is the scatter and line that is relevant to this matter

def plotGraph(self, df=None):
    # Get parameters to plot
    x = str(self.ui.comboBox_x_axis.currentText())
    y = str(self.ui.comboBox_y_axis.currentText())
    # Define axis types
    try:
        x_axis_type = str(
            self.ui.comboBox_plot_scale.currentText()).split('-')[0]
        y_axis_type = str(
            self.ui.comboBox_plot_scale.currentText()).split('-')[1]
    except:
        x_axis_type = 'auto'
        y_axis_type = 'auto'
    # Define kind of graph
    kind = str(self.ui.comboBox_plot_style.currentText())
    # For bar chart define groups
    group = str(self.ui.comboBox_group.currentText())
    # Prepare data for plot
    if (kind == 'bar' and group != "Don't group"):
        data = df[[x, y, group]]
    else:
        data = df[[x, y]]
        data = data.sort_values(x, axis=0)
    # Dynamically define plot size
    width = round(self.ui.webViewer.frameGeometry().width())
    height = round(self.ui.webViewer.frameGeometry().height())
    # Plot and save html
    self.plot = self.graph.plot(
        data, kind, x_axis_type, y_axis_type, width, height)
    self.plot_num = 1
    # Display it at QWebEngineView
    self.ui.webViewer.setUrl(QtCore.QUrl(
        "file:///C:/Users/eandrade_brp/Documents/git/tl-data-viewer/plot.html"))

Here is the Graph class that handles all the bokeh plots (I omitted some non necessary code)

class Graph(object):
    """docstring for ClassName"""

    def __init__(self, file_name="plot.html"):
        super(Graph, self).__init__()
        output_file(file_name)

    def plot(self, data, kind, x_axis_type, y_axis_type, width, height):
        p = None
        if kind == 'scatter' or kind == 'line':
            layout, p = self.createFigure(
                data, kind, x_axis_type, y_axis_type, width, height)
        elif kind == 'bar':
            layout = self.plot_Bar(data, width, height)
        elif kind == 'pizza':
            layout = self.plot_Pizza(
                data, width, height)
        # Show/save
        save(layout)
        return p

    def createFigure(self, data, kind, x_axis_type, y_axis_type, width, height):
        source, xdata, ydata, xvalues, yvalues = self.prepare_data(data)
        # Define tool
        tools = "pan, box_zoom, lasso_select, undo, redo"
        wheel_zoom = WheelZoomTool()
        hover = HoverTool(
            tooltips=[
                (data.columns[0],          '$x'),
                (data.columns[1],          '$y')],
            mode='mouse')
        # Create first figure and customize
        fig1 = figure(title="{} vs {}" .format(ydata, xdata), tools=tools,
                      x_axis_type=x_axis_type, y_axis_type=y_axis_type,
                      toolbar_location="right", plot_width=round(0.9 * width),
                      plot_height=round(0.75 * height))
        fig1.add_tools(wheel_zoom)
        fig1.add_tools(hover)
        fig1.toolbar.active_scroll = wheel_zoom
        fig1.background_fill_color = "beige"
        fig1.background_fill_alpha = 0.4

        # Create second figure and customize
        fig2 = figure(title='Overview', title_location="left",
                      x_axis_type=x_axis_type, y_axis_type=y_axis_type,
                      tools='', plot_width=round(0.9 * width), plot_height=round(0.25 * height))
        fig2.xaxis.major_tick_line_color = None
        fig2.xaxis.minor_tick_line_color = None
        fig2.yaxis.major_tick_line_color = None
        fig2.yaxis.minor_tick_line_color = None
        fig2.xaxis.major_label_text_color = None
        fig2.yaxis.major_label_text_color = None

        # Add View box to second figure
        rect = Rect(x='x', y='y', width='width', height='height', fill_alpha=0.1,
                    line_color='black', fill_color='black')
        fig2.add_glyph(source, rect)

        # Add JS callBacks
        self.JS_linkPlots(fig1, source)

        # Plots
        plots = self.plot_continuous(source, xvalues, yvalues, fig1, kind)
        self.plot_continuous(source, xvalues, yvalues, fig2, kind)
        s2 = ColumnDataSource(data=dict(ym=[0.5, 0.5]))
        fig1.line(x=[0, 1], y='ym', color="orange",
                  line_width=5, alpha=0.6, source=s2)

        # Add legends
        legend = Legend(items=[
            (ydata, plots)],
            location=(0, 0),
            click_policy="mute")
        # Add legend to fig layout
        fig1.add_layout(legend, 'below')
        # Layout
        layout = col(fig1, fig2)
        return layout, fig1

    def plot_continuous(self, source, xvalues, yvalues, fig, kind, color=0):
        if kind == 'scatter':
            s = fig.scatter(
                xvalues, yvalues,
                fill_color='white', fill_alpha=0.6,
                line_color=Spectral10[color], size=8,
                selection_color="firebrick",
                nonselection_fill_alpha=0.2,
                nonselection_fill_color="blue",
                nonselection_line_color="firebrick",
                nonselection_line_alpha=1.0)
            return [s]

        elif kind == 'line':
            l = fig.line(
                xvalues, yvalues, line_width=2, color=Spectral10[color], alpha=0.8,
                muted_color=Spectral10[color], muted_alpha=0.2)

            s = fig.scatter(
                xvalues, yvalues,
                fill_color="white", fill_alpha=0.6,
                line_color=Spectral10[color], size=8,
                selection_color="firebrick",
                nonselection_fill_alpha=0.2,
                nonselection_fill_color="blue",
                nonselection_line_color="firebrick",
                nonselection_line_alpha=1.0)
            return [s, l]
        else:
            raise 'Wrong type of plot'

    def prepare_data(self, data):
        xdata = data.columns[0]
        xvalues = data[xdata]
        ydata = data.columns[1]
        yvalues = data[ydata]
        source = ColumnDataSource(data)
        return source, xdata, ydata, xvalues, yvalues
1

There are 1 answers

0
bigreddot On BEST ANSWER

First, a disclaimer: Bokeh makes no claim to function, either fully or partially, with Qt browser widgets. We are simply not equipped to be able to maintain that claim rigorously under continuous testing, therefore we cannot make it. If anyone would ever like to step in as a maintainer of that functionality, it's possible in the future that we can make stronger support claims.


Bokeh uses a third party library Hammer.js to provide uniform low-level event handling across different platforms. Bokeh expects that the events that are generated have a pageX and pageY attributes. It appears that Qt's browser widget does not satisfy this expectation, leading to the error you are seeing. It's possible that updating the version of Hammer used by Bokeh might fix the problem. It's possible that a workaround could be introduced. In any case, it would require new work on BokehJS itself.

The short answer is: this interacive legend probably just is not going to work on Qt. As a workaround, use Bokeh widgets or Qt Widgets to high and show glyphs, and do not rely on the interactive legend capability.

Longer term: Wo could look into some of the ideas suggested above. But we would need assistance to do do. We do not have the bandwidth, ability, or experience to build Qt apps ourselves to test potential fixes. If you have the ability to work together with a core dev on finding a solution, please feel free to make an issue on the issue tracker.