Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix resizing plotly web clients #4806

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Fix resizing plotly web clients #4806

wants to merge 6 commits into from

Conversation

timtmok
Copy link
Contributor

@timtmok timtmok commented Sep 24, 2024

Address #4245

Sets Plotly to use its browser renderer. When calling show(), we intercept and send an event so that the URL is displayed in a webview in the Plots View instead of the viewer (or in the Viewer if the preference is set).

Plotly's browser renderer fills width and height to 100% when no plot size is defined compared to the JupyterLab renderer (or vscode?) that defaults to Plots View's width and height 450px. Underneath, the browser renderer sets responsive: True for the config, which will set width and height to 100%.

I'm not sure about the proxy part. We don't create one for the Viewer so I'm not sure if this will need more work when using this in Posit Workbench.

QA Notes

Code to generate a Plotly plot. pip install plotly before running.

import plotly.express as px
fig = px.bar(x=["a", "b", "c"], y=[1, 3, 2])
fig.show()

Disable this setting so the plot shows in Plots instead of the Viewer.
image

Detect plotly plots and send as an HTML plot event
Shows the localhost plot in Plots View
@timtmok
Copy link
Contributor Author

timtmok commented Sep 25, 2024

I'm working adding a test similar to what is there for Bokeh

Copy link
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that plotly plots are resizing with the plots pane! My main concern is about circumventing the proxy server.

Comment on lines -165 to +171
// Start an HTML proxy server for the file
const uri = await this.startHtmlProxyServer(e.path);
if (e.path.endsWith('.html') || e.path.endsWith('.htm')) {
// Start an HTML proxy server for the file
uri = await this.startHtmlProxyServer(e.path);
} else {
uri = URI.parse(e.path);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work without this change? This looks like it won't create a proxy server which I think wouldn't work on Workbench.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part I wasn't sure if it would work. When opening other URLs in the viewer, it doesn't proxy it. I'll try it out but I agree that I don't think it would work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to work when running Positron server but it's all local. I think it's only working since the deepest iframe loads the plotly url.

@sharon-wang I think it probably needs a tweak on creating the proxy server or a slightly different type of proxy server. The one used for HTML files didn't seem to work with the localhost URL that plotly produces (file URI vs. localhost URL).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have some example plotly URI/URLs? On main, python plotly plots work on Server Web and Workbench, so I wonder what changes to the URI/URLs are occurring?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http://127.0.0.1:6728/

It's loading something like that with a random port. Before this change, it would load an HTML file and go through the HTMLProxy. So I think it'll need to load the URL with a proxy as it did before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timtmok could you please verify if reverting this change to bypass the proxy and bringing in the changes from #4855 still works with this PR?

return True
# pass back to webbrowser's list of browsers to open up the link
return False


def _is_module_function(self, module_name, function_name):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This could be a standalone function since self is never used.

if module:
for frame_info in inspect.stack():
if (
inspect.getmodule(frame_info.frame, frame_info.filename) == module
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wrote this code, and I've wondered if it would be safer to check only the module part and not the function. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed to make sense to check the function too but I don't think I know enough about Python to foresee any problems. I suppose removing the function check would handle any new calls that Bokeh or Plotly might add.

Then it could work in that scenario without having to do anything. Keeping the check would definitely not work in that case so there's nothing to lose?

if os.name == "nt":
url = urlparse(url).netloc or urlparse(url).path

return self._comm.send_event(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think webbrowser expects this method to return a boolean: True if we've handled the request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be calling _send_show_html_event here?

if is_plot:
if os.name == "nt":
url = urlparse(url).netloc or urlparse(url).path
event = ShowUrlParams(url=url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
event = ShowUrlParams(url=url)


def _send_show_html_event(self, url, is_plot):
if self._comm is None:
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should log a warning here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

return False


def _send_show_html_event(self, url, is_plot):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add type annotations to this and _is_module_function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotations added

Comment on lines 216 to 217
if os.name == "nt":
url = urlparse(url).netloc or urlparse(url).path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be moved to _send_show_html_event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved it into the new function

@timtmok
Copy link
Contributor Author

timtmok commented Sep 26, 2024

I added a unit test but it doesn't work locally. It hangs after making the call to send the comm event. Not really sure what's going on here?

@timtmok
Copy link
Contributor Author

timtmok commented Sep 27, 2024

I had to mock HTTPServer.handle_request() to stop the test from hanging.

Copy link
Collaborator

@jmcphers jmcphers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not build/test this since others have.

A little nitpicky, but I'd prefer not to overload ShowHtmlFile if we're sending a URL rather than a HTML file path. There's ShowUrl already; we could add a param to that method to indicate a plot, or create a different event to handle this brand of URLs.

If we do need to overload ShowHtmlFile, maybe we could check for file: / http: URIs rather than checking the file extension?

@sharon-wang
Copy link
Member

A little nitpicky, but I'd prefer not to overload ShowHtmlFile if we're sending a URL rather than a HTML file path. There's ShowUrl already; we could add a param to that method to indicate a plot, or create a different event to handle this brand of URLs.

If we do need to overload ShowHtmlFile, maybe we could check for file: / http: URIs rather than checking the file extension?

@jmcphers I think that change will be reverted if the fix in #4855 works for Tim -- the approach in #4855 is to check the URI scheme in startHtmlProxyServer and start the appropriate proxy server: https://github.com/posit-dev/positron/pull/4855/files#diff-b42b14eff9e1b23c8e4bbe70783a6a4065dd0ecc89d250f1012e006367079366

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants