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
node.js integration and static image generation #3047
Changes from 24 commits
15401cc
de7cc11
a2c053f
b67b80c
96fcc68
af8c682
c16d186
75657c1
dc5aa82
019fcd5
e756bcc
a839346
eeb3871
babd570
a6b60c0
3ffc50a
4a1af80
5fad6dd
244b979
ae9cb9f
851cc93
6e51490
468c775
7f69c3c
3eaf4a0
1b9e5b5
a60b37c
df12d4e
6e3dfc5
1f4e178
cc6077b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,6 @@ | |
|
||
from __future__ import absolute_import | ||
|
||
import re | ||
import uuid | ||
from warnings import warn | ||
|
||
|
@@ -130,10 +129,8 @@ def components(plot_objects, resources=None, wrap_script=True, wrap_plot_info=Tr | |
|
||
with _ModelInDocument(plot_objects): | ||
(docs_json, render_items) = _standalone_docs_json_and_render_items(plot_objects) | ||
custom_models = _extract_custom_models(plot_objects) | ||
|
||
script = _script_for_render_items(docs_json, render_items, custom_models=custom_models, | ||
websocket_url=None, wrap_script=wrap_script) | ||
script = _script_for_render_items(docs_json, render_items, websocket_url=None, wrap_script=wrap_script) | ||
script = encode_utf8(script) | ||
|
||
if wrap_plot_info: | ||
|
@@ -153,47 +150,6 @@ def components(plot_objects, resources=None, wrap_script=True, wrap_plot_info=Tr | |
else: | ||
return script, tuple(results) | ||
|
||
def _escape_code(code): | ||
""" Escape JS/CS source code, so that it can be embbeded in a JS string. | ||
|
||
This is based on https://github.com/joliss/js-string-escape. | ||
""" | ||
def escape(match): | ||
ch = match.group(0) | ||
|
||
if ch == '"' or ch == "'" or ch == '\\': | ||
return '\\' + ch | ||
elif ch == '\n': | ||
return '\\n' | ||
elif ch == '\r': | ||
return '\\r' | ||
elif ch == '\u2028': | ||
return '\\u2028' | ||
elif ch == '\u2029': | ||
return '\\u2029' | ||
|
||
return re.sub(u"""['"\\\n\r\u2028\u2029]""", escape, code) | ||
|
||
def _extract_custom_models(plot_objects): | ||
custom_models = {} | ||
|
||
def extract_from_model(model): | ||
for r in model.references(): | ||
impl = getattr(r.__class__, "__implementation__", None) | ||
if impl is not None: | ||
name = r.__class__.__name__ | ||
impl = "['%s', {}]" % _escape_code(impl) | ||
custom_models[name] = impl | ||
|
||
for o in plot_objects: | ||
if isinstance(o, Document): | ||
for r in o.roots: | ||
extract_from_model(r) | ||
else: | ||
extract_from_model(o) | ||
|
||
return custom_models | ||
|
||
def notebook_div(plot_object): | ||
''' Return HTML for a div that will display a Bokeh plot in an | ||
IPython Notebook | ||
|
@@ -215,11 +171,8 @@ def notebook_div(plot_object): | |
|
||
with _ModelInDocument(plot_object): | ||
(docs_json, render_items) = _standalone_docs_json_and_render_items([plot_object]) | ||
custom_models = _extract_custom_models([plot_object]) | ||
|
||
script = _script_for_render_items(docs_json, render_items, | ||
custom_models=custom_models, | ||
websocket_url=None) | ||
script = _script_for_render_items(docs_json, render_items, websocket_url=None) | ||
|
||
item = render_items[0] | ||
|
||
|
@@ -285,9 +238,8 @@ def file_html(plot_objects, | |
|
||
(docs_json, render_items) = _standalone_docs_json_and_render_items(plot_objects) | ||
title = _title_from_plot_objects(plot_objects, title) | ||
custom_models = _extract_custom_models(plot_objects) | ||
return _html_page_for_render_items(resources, docs_json, render_items, title=title, | ||
custom_models=custom_models, websocket_url=None, | ||
websocket_url=None, | ||
js_resources=js_resources, css_resources=css_resources, | ||
template=template, template_variables=template_variables, | ||
use_widgets=_use_widgets(plot_objects)) | ||
|
@@ -400,28 +352,24 @@ def autoload_server(plot_object, app_path="/", session_id=DEFAULT_SESSION_ID, ur | |
|
||
return encode_utf8(tag) | ||
|
||
def _script_for_render_items(docs_json, render_items, websocket_url, | ||
custom_models, wrap_script=True): | ||
# this avoids emitting the "register custom models" code at all | ||
# just to register an empty set | ||
if (custom_models is not None) and len(custom_models) == 0: | ||
custom_models = None | ||
|
||
plot_js = _wrap_in_function( | ||
DOC_JS.render( | ||
custom_models=custom_models, | ||
websocket_url=websocket_url, | ||
docs_json=serialize_json(docs_json), | ||
render_items=serialize_json(render_items) | ||
def _script_for_render_items(docs_json, render_items, websocket_url, wrap_script=True): | ||
def plot_js(docs_json): | ||
return _wrap_in_function( | ||
DOC_JS.render( | ||
websocket_url=websocket_url, | ||
docs_json=serialize_json(docs_json), | ||
render_items=serialize_json(render_items), | ||
) | ||
) | ||
) | ||
|
||
if wrap_script: | ||
return SCRIPT_TAG.render(js_code=plot_js) | ||
id = str(uuid.uuid4()) | ||
return SCRIPT_TAG.render(docs_json_id=id, docs_json=serialize_json(docs_json), js_code=plot_js(id)) | ||
else: | ||
return plot_js | ||
return plot_js(docs_json) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this refactor is incomplete and we end up with one function that really does two separate things. As it is, the function is named and expected elsewhere to return one hunk of script, which is optionally wrapped in one
The simplest thing for this PR is probably to drop the "externalize docs json into its own Going from one script to a list of scripts will break the If we do have two codepaths, I'd rather not overload |
||
|
||
def _html_page_for_render_items(resources, docs_json, render_items, title, websocket_url, | ||
custom_models, js_resources=None, css_resources=None, | ||
js_resources=None, css_resources=None, | ||
template=FILE, template_variables={}, use_widgets=True): | ||
if title is None: | ||
title = DEFAULT_TITLE | ||
|
@@ -449,7 +397,7 @@ def _html_page_for_render_items(resources, docs_json, render_items, title, webso | |
css_resources = css_resources.use_widgets(use_widgets) | ||
bokeh_css = css_resources.render_css() | ||
|
||
script = _script_for_render_items(docs_json, render_items, websocket_url, custom_models) | ||
script = _script_for_render_items(docs_json, render_items, websocket_url) | ||
|
||
template_variables_full = template_variables.copy() | ||
|
||
|
@@ -597,7 +545,7 @@ def server_html_page_for_models(session_id, model_ids, resources, title, websock | |
}) | ||
|
||
return _html_page_for_render_items(resources, {}, render_items, title, | ||
websocket_url=websocket_url, custom_models=None) | ||
websocket_url=websocket_url) | ||
|
||
def server_html_page_for_session(session_id, resources, title, websocket_url): | ||
elementid = str(uuid.uuid4()) | ||
|
@@ -609,4 +557,4 @@ def server_html_page_for_session(session_id, resources, title, websocket_url): | |
}] | ||
|
||
return _html_page_for_render_items(resources, {}, render_items, title, | ||
websocket_url=websocket_url, custom_models=None) | ||
websocket_url=websocket_url) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -101,6 +101,12 @@ def output_file(filename, title="Bokeh Plot", autosave=False, mode="inline", roo | |
root_dir=root_dir | ||
) | ||
|
||
def output_png(filename, autosave=False): | ||
_state.output_png( | ||
filename, | ||
autosave=autosave, | ||
) | ||
|
||
def output_notebook(resources=None, verbose=False, hide_banner=False): | ||
''' Configure the default output state to generate output in | ||
Jupyter/IPython notebook cells when :func:`show` is called. | ||
|
@@ -242,17 +248,21 @@ def _show_with_state(obj, state, browser, new): | |
|
||
if state.notebook: | ||
_show_notebook_with_state(obj, state) | ||
|
||
elif state.session_id and state.server_url: | ||
_show_server_with_state(obj, state, new, controller) | ||
|
||
if state.file: | ||
elif state.file: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the code was |
||
_show_file_with_state(obj, state, new, controller) | ||
elif state.png: | ||
_show_png_with_state(obj, state, new, controller) | ||
|
||
def _show_file_with_state(obj, state, new, controller): | ||
save(obj, state=state) | ||
controller.open("file://" + os.path.abspath(state.file['filename']), new=_new_param[new]) | ||
|
||
def _show_png_with_state(obj, state, new, controller): | ||
savepng(obj, state=state) | ||
controller.open("file://" + os.path.abspath(state.png['filename']), new=_new_param[new]) | ||
|
||
def _show_notebook_with_state(obj, state): | ||
if state.session_id: | ||
push(state=state) | ||
|
@@ -346,6 +356,47 @@ def _save_helper(obj, filename, resources, title, validate): | |
with io.open(filename, "w", encoding="utf-8") as f: | ||
f.write(decode_utf8(html)) | ||
|
||
def savepng(obj, filename=None, state=None, validate=True): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should it be Why did you go with a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Given that this is pylab-style API, I think not, but I'm fine with adding one. |
||
if state is None: | ||
state = _state | ||
|
||
if filename is None and state.png: | ||
filename = state.png['filename'] | ||
|
||
if filename is None: | ||
raise RuntimeError("savepng() called but no filename was supplied and output_png(...) was never called, nothing saved") | ||
|
||
with _ModelInDocument(obj): | ||
if isinstance(obj, Component): | ||
doc = obj.document | ||
elif isinstance(obj, Document): | ||
doc = obj | ||
else: | ||
raise RuntimeError("Unable to save object of type '%s'" % type(obj)) | ||
|
||
if validate: | ||
doc.validate() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the code from |
||
|
||
from subprocess import Popen, PIPE | ||
from os.path import join, dirname | ||
|
||
from .resources import Resources | ||
|
||
resources = Resources(mode="absolute-dev") | ||
|
||
from .embed import _check_plot_objects, _standalone_docs_json_and_render_items | ||
from ._json_encoder import serialize_json | ||
|
||
plot_objects = [obj] | ||
plot_objects = _check_plot_objects(plot_objects) | ||
(docs_json, render_items) = _standalone_docs_json_and_render_items(plot_objects) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These underscore-prefixed functions aren't really meant to leave |
||
|
||
script = join(dirname(dirname(__file__)), "bokehjs", "render.js") | ||
cmd = "node %s --filename=%s --log-level=%s" % (script, filename, resources.log_level) | ||
|
||
proc = Popen(cmd, stdin=PIPE, shell=True) | ||
proc.communicate(input=serialize_json(docs_json)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need our |
||
|
||
# this function exists mostly to be mocked in tests | ||
def _push_to_server(websocket_url, document, session_id, io_loop): | ||
session = push_session(document, session_id=session_id, url=websocket_url, io_loop=io_loop) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will cause us problems; models code is not part of Document. Here we are tacking an extra payload on to Document that we'd like to go to JavaScript, because we happen to know we'll send the Document to JavaScript.
It's an unexpected side effect that
Document.from_json
on the coffee side loads custom models, it should only fill inDocument
, not have surprising side effects.What I think would be cleaner: in
_standalone_docs_json_and_render_items
, redefinedocs_json
to include the models (maybe demote the current dict of docs, so it's{ 'models' : { ... }, 'docs' : current_docs_json_here }
and then inBokeh.embed.embed_items
of course the code has to be changed to match.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another advantage of doing it in
_standalone_docs_json_and_render_items
is that if there's multiple docs we only send the models once