def facet_render(self, shape, **config): ''' Render chart in facets Parameters =========== shape = string , shape of the facets - rect, list , matrix , tree , circle config = xxargs , configurations - fields, padding Return ======== IPython.core.display.Javascript ''' # Should be modified in a better form layout_code = self.get_layout_code() data_code = 'var data = %s; \n chart.data(data);'%self.chart_data + '\n' scale_code = self.create_scale_code() #item_code = 'chart.interval().position(\'x*y\');' axis_code = self.create_axis_code() item_code = self.create_item_code() coordinate_code = str(self.coordinate_sys) + ';\n' tooltip_code = self.tooltip_details+';\n' legend_code = self.create_legend_code() annotate_code = self.create_annotate_code() # Have to study how annotations are handled in facets element_code = '' additional_code = self.additional_code render_code = 'chart.render();' each_view = '(chart)=>{%s}'%(item_code+annotate_code) config['eachView'] = 'each_view' facet_code = 'chart.facet("%s",%s)'%(shape,config)+';\n' facet_code = facet_code.replace("'each_view'",each_view) final_code = layout_code+data_code+scale_code+axis_code+facet_code+coordinate_code+tooltip_code+element_code+legend_code+annotate_code+additional_code+render_code return Javascript(get_notebook_code(final_code))
def VideoCapture(): js = Javascript(''' async function create(){ div = document.createElement('div'); document.body.appendChild(div); video = document.createElement('video'); video.setAttribute('playsinline', ''); div.appendChild(video); stream = await navigator.mediaDevices.getUserMedia({video: {facingMode: "environment"}}); video.srcObject = stream; await video.play(); canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); div_out = document.createElement('div'); document.body.appendChild(div_out); img = document.createElement('img'); div_out.appendChild(img); } async function capture(){ return await new Promise(function(resolve, reject){ pendingResolve = resolve; canvas.getContext('2d').drawImage(video, 0, 0); result = canvas.toDataURL('image/jpeg', 0.8); pendingResolve(result); }) } function showing(imgb64){ img.src = "data:image/jpg;base64," + imgb64; } ''') display(js)
def plot_wordcloud(wordcloud): return Javascript(""" var fill = d3.scale.category20b(); var cloudNode = $('<div id="wordcloud"></div>'); element.append(cloudNode); var wordData = JSON.parse('%s'); console.log(wordData); function draw(words) { d3.select("#wordcloud").append("svg") .attr("width", 600) .attr("height", 502) .append("g") .attr("transform", "translate(300,160)") .selectAll("text") .data(words) .enter().append("text") .style("font-size", function (d) { return d.size + "px"; }) .style("font-family", "impact") .style("fill", function (d, i) { return fill(i); }) .attr("text-anchor", "middle") .attr("transform", function (d) { return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; }) .text(function (d) { return d.text; }); } console.log($("#wordcloud")); d3.layout.cloud().size([600, 502]) .timeInterval(10) .words(wordData) .padding(1) .rotate(function () { return 0; }) .font('impact') .fontSize(function (d) { return d.size; }) .on("end", draw) .start(); """ % json.dumps(wordcloud), lib=libs)
def take_photo(filename='photo.jpg', quality=0.8): from IPython.display import display, Javascript from google.colab.output import eval_js from base64 import b64decode js = Javascript(''' async function takePhoto(quality) { const div = document.createElement('div'); const capture = document.createElement('button'); capture.textContent = '사진캡쳐'; div.appendChild(capture); const video = document.createElement('video'); video.style.display = 'block'; const stream = await navigator.mediaDevices.getUserMedia({video: true}); document.body.appendChild(div); div.appendChild(video); video.srcObject = stream; await video.play(); // Resize the output to fit the video element. google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true); // Wait for Capture to be clicked. await new Promise((resolve) => capture.onclick = resolve); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); stream.getVideoTracks()[0].stop(); div.remove(); return canvas.toDataURL('image/jpeg', quality); } ''') display(js) data = eval_js('takePhoto({})'.format(quality)) binary = b64decode(data.split(',')[1]) with open(filename, 'wb') as f: f.write(binary) return filename
def run(self): while True: self.triggerEvent.wait() with self.lock: self.triggerEvent.clear() if bool(self.progressData): progressData = self.progressData self.progressData = OrderedDict() else: progressData = OrderedDict() if bool(progressData): js = "" for data in progressData.values(): channel = data["channel"] if channel=="jobStart": js += _env.getTemplate("sparkJobProgressMonitor/addJobTab.js").render( prefix=self.prefix, data=data, overalNumTasks=reduce(lambda x,y:x+y["numTasks"], data["stageInfos"], 0) ) elif channel=="stageSubmitted": js += _env.getTemplate("sparkJobProgressMonitor/updateStageStatus.js").render( prefix=self.prefix, stageId=data["stageInfo"]["stageId"], status="Submitted", host=None ) elif channel=="taskStart": js += _env.getTemplate("sparkJobProgressMonitor/taskStart.js").render( prefix=self.prefix, data=data, increment = data["increment"] ) js += "\n" js += _env.getTemplate("sparkJobProgressMonitor/updateStageStatus.js").render( prefix=self.prefix, stageId=data["stageId"], status="Running", host="{0}({1})".format(data["taskInfo"]["executorId"],data["taskInfo"]["host"] ) ) elif channel=="stageCompleted": js += _env.getTemplate("sparkJobProgressMonitor/updateStageStatus.js").render( prefix=self.prefix, stageId=data["stageInfo"]["stageId"], status="Completed", host=None ) elif channel=="jobEnd": js += _env.getTemplate("sparkJobProgressMonitor/jobEnded.js").render( prefix=self.prefix, jobId=data["jobId"] ) js += "\n" display(Javascript(js)) time.sleep(0.5)
def enable_notebook(): """Enable IPython notebook widgets to be displayed. This function should be called before using TrajectoryWidget. """ libs = [ 'iview.js', 'surface.min.js', 'objexporter.js', 'filesaver.js', 'context.js' ] fns = [ resource_filename('mdtraj', os.path.join('html', 'static', f)) for f in libs ] install_nbextension(fns, verbose=0) display(_REQUIRE_CONFIG) widgets = ['widget_trajectory.js', 'widget_imagebutton.js'] for fn in widgets: fn = resource_filename('mdtraj', os.path.join('html', 'static', fn)) display(Javascript(filename=fn))
def report(self, bytes_so_far, chunk_size, total_size): if useProgressMonitor: if bytes_so_far == 0: display( HTML(""" <div> <span id="pm_label{0}">Starting download...</span> <progress id="pm_progress{0}" max="100" value="0" style="width:200px"></progress> </div>""".format(self.prefix))) else: percent = float(bytes_so_far) / total_size percent = round(percent * 100, 2) display( Javascript(""" $("#pm_label{prefix}").text("{label}"); $("#pm_progress{prefix}").attr("value", {percent}); """.format(prefix=self.prefix, label="Downloaded {0} of {1} bytes".format( bytes_so_far, total_size), percent=percent)))
def set(self, fn: str, *args): test = GGBApi.isSetterShort(fn) if test is not False: fn = test if GGBApi.isSetter(fn): jsArgs = [] # Don't use isinstance() to avoid bool as a subtype of int for arg in args: if type(arg) is int or type(arg) is float: jsArgs.append(str(arg)) elif type(arg) is bool: jsArgs.append(str(arg).lower()) else: jsArgs.append('\"%s\"' % arg) jsArgsString = ', '.join(jsArgs) display( Javascript('%s.%s(%s)' % (self.instanceName, fn, jsArgsString))) return self
def auto(self, enabled=True, **kwargs): """ Method to enable or disable automatic capture, allowing you to simultaneously set the instance parameters. """ self.notebook_name = "{notebook}" self._timestamp = tuple(time.localtime()) kernel = r'var kernel = IPython.notebook.kernel; ' nbname = r"var nbname = IPython.notebook.get_notebook_name(); " nbcmd = (r"var name_cmd = '%s.notebook_name = \"' + nbname + '\"'; " % self.namespace) cmd = (kernel + nbname + nbcmd + "kernel.execute(name_cmd); ") display(Javascript(cmd)) time.sleep(0.5) self._auto=enabled self.set_param(**kwargs) tstamp = time.strftime(" [%Y-%m-%d %H:%M:%S]", self._timestamp) print("Automatic capture is now %s.%s" % ('enabled' if enabled else 'disabled', tstamp if enabled else ''))
def display(shape): payload: List[Dict[str, Any]] = [] if isinstance(shape, Shape): payload.append( dict( shape=toString(shape), color=DEFAULT_COLOR, position=[0, 0, 0], orientation=[0, 0, 0], )) elif isinstance(shape, Assembly): payload = toJSON(shape) else: raise ValueError(f"Type {type(shape)} is not supported") code = TEMPLATE.format(data=dumps(payload), element="element", ratio=0.5) return Javascript(code)
def init(): output_notebook() display(Javascript(_init_js)) but = '<img src="resources/show.png" width="34" height="25" style="display: inline" alt="Slideshow button" title="Enter/Exit RISE Slideshow">' txt = Div(text='<h2>You can now start the slideshow!</h3>' + f'<h3 style="margin: 0.5em 0;">Just click the RISE slideshow button above - the one that looks like: {but}<br/>' + '(or you can press alt+R on your keyboard instead if you prefer).</h3>') clearbutton = Button(label="Clear") clearbutton.js_on_click(CustomJS(code='primes_clear();')) cleartext = Paragraph(text='Clear all plots and outputs (e.g. before restarting slideshow).') increm = Toggle(label='Incremental', active=True) increm.js_on_click(CustomJS(code='primes_incremental(cb_obj.active)')) incremtext = Paragraph(text='Update timing plots incrementally (disable for static slide show).') repeats = Slider(start=1, end=10, value=3) repeats.js_on_change('value', CustomJS(code='primes_repeats(cb_obj.value)')) repeatstext = Paragraph(text='Repeats for timing measurements (higher is more accurate, but slower).') controls = layout([[clearbutton, cleartext], [increm, incremtext], [repeats, repeatstext]]) show(column(txt, controls, sizing_mode='stretch_width'))
def demo_js_scope_e(): ''' set e to 99 and run kernel.do_one_iteration() ''' ip = get_ipython() ip.kernel.do_one_iteration() # ip.kernel.do_one_iteration() js = ''' IPython.notebook.kernel.execute("e=99"); ''' js = '\n'.join([line.strip() for line in js.split('\n') if line.strip !='']) display(Javascript(data=js)) ip.kernel.do_one_iteration() return None
def parse_client_data(callback): roomId = random.randint(0, 99999999999999999999) url = _WS_BaseUrl.format(roomId) def get_JS_WsContent(url): return """ var websocket = new WebSocket("%s"); websocket.onopen = function (event) { websocket.send(JSON.stringify({ "%s": window.location.href })); }; """ % (url, Constants.CLIENT_DATA_URL) ws = websocket.create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE}) display(Javascript(get_JS_WsContent(url))) data = json.loads(ws.recv()) ws.close() callback(data)
def create_code_cell(code='', where='below'): """Create a code cell in the IPython Notebook. Found at https://github.com/ipython/ipython/issues/4983 Parameters code: unicode Code to fill the new code cell with. where: unicode Where to add the new code cell. Possible values include: at_bottom above below""" encoded_code = bytes_to_str(base64.b64encode(str_to_bytes(code))) display( Javascript(""" var code = IPython.notebook.insert_cell_{0}('code'); code.set_text(atob("{1}")); """.format(where, encoded_code)))
def one_hour_warning(self): display( Javascript(""" require( ["base/js/dialog"], function(dialog) { dialog.modal({ title: "1 HOUR LEFT", body: "Just 1 hour remaining. How's it going? Any exciting results? \ Remember not to focus too much time on any one thing. \ Consider wrapping up your main section in the next 30-45 mins to give you time \ for the conclusion/results section.", buttons: { 'OK': {} } }); } ); """))
def import_html_to_head(self, html_hrefs): """Imports given external HTMLs (supported through webcomponents) into the head of the document. On load of webcomponentsjs, import given HTMLs. If HTML import is already supported, skip loading webcomponentsjs. No matter how many times an HTML import occurs in the document, only the first occurrence really embeds the external HTML. In a notebook environment, the body of the document is always changing due to cell [re-]execution, deletion and re-ordering. Thus, HTML imports shouldn't be put in the body especially the output areas of notebook cells. """ try: from IPython.display import Javascript from IPython.display import display_javascript display_javascript( Javascript(_HTML_IMPORT_TEMPLATE.format(hrefs=html_hrefs))) except ImportError: pass # NOOP if dependencies are not available.
def submit_task(session, course_id, lab_id, task_id): url = "%s/users/%s/courses/%s/labs/%s/tasks/%s" % ( session.endpoint, session.user, course_id, lab_id, task_id) js = common_js + """ var url = '%s' console.log("URL", url) var task_id = "%s"; var task_cells = get_cells_with_metadatakey(task_id) var xhttp = new XMLHttpRequest(); xhttp.open("POST", url, false); xhttp.onreadystatechange = function() { var r = JSON.parse(this.response) var text = "### "+task_id+" submission result" console.log(this.response) if ("error" in r) { text += "\\n\\n### **error**: <font color='red'>"+r["error"]+"</font>" text += "\\n\\n```"+r["traceback"]+"```" } else { var grade = r["grade"] var msg = r["message"] var code = r["submission_stamp"] text += "\\n |grade| msg |" text += "\\n |:-:|:-|" text += "\\n |"+grade+"|"+msg+"|" text += "\\n\\n**submission stamp**: "+code } var cell = get_current_cell() delete_cell_with_content(task_id+" submission result") var cell = insert_cell_after_current("markdown") cell.set_text(text) cell.render() }; xhttp.setRequestHeader("Content-type", "application/json"); xhttp.setRequestHeader('Mooc-Token', '%s'); xhttp.send(JSON.stringify({"submission_content": task_cells})); """ % (url, task_id, session.token) display(Javascript(js))
def build_job_viewer(): """Builds the job viewer widget Returns: widget: Job viewer. """ acc = widgets.Accordion( children=[ widgets.VBox( layout=widgets.Layout(max_width="710px", min_width="710px")) ], layout=widgets.Layout( width="auto", max_width="750px", max_height="500px", overflow_y="scroll", overflow_x="hidden", ), ) acc.set_title(0, "IBMQ Jobs") acc.selected_index = None acc.layout.visibility = "hidden" display(acc) acc._dom_classes = ["job_widget"] display( Javascript(""" const isLab = window['Jupyter'] === undefined; const notebook = document.querySelector( isLab ? 'div.jp-Notebook' : '#site'); const jobWidget = document.querySelector('div.job_widget'); notebook.prepend(jobWidget); jobWidget.style.zIndex = '999'; jobWidget.style.position = isLab ? 'sticky' : 'fixed'; jobWidget.style.boxShadow = '5px 5px 5px -3px black'; jobWidget.style.opacity = '0.95'; if (isLab) { jobWidget.style.top = '0'; jobWidget.style.left = '0'; } """)) acc.layout.visibility = "visible" return acc
def _start_server_and_view_report(report_directory: str, mode: str, port: int) -> None: """ Serve the report to the user using a web server. :param report_directory: The directory created report is saved :param mode: server mode ('server': will open a new tab in your default browser, 'js': will open a new tab in your browser using a different method, 'jupyter': will open the report application in your notebook). default: 'server' :param port: the server port. default: random between (1024-49151) :return: None """ import multiprocessing as mp import time import importlib.util spec = importlib.util.spec_from_file_location( "app", f'{report_directory}/app.py') app = importlib.util.module_from_spec(spec) spec.loader.exec_module(app) try: p = mp.Process(target=app.run_application, args=(port, report_directory)) p.start() time.sleep(1.0) url = f'http://127.0.0.1:{port}/' if mode == 'server': import webbrowser webbrowser.open(url) elif mode == 'js': from IPython.core.display import display from IPython.display import Javascript display(Javascript(f'window.open("{url}");')) else: from IPython.display import IFrame from IPython.core.display import display display(IFrame(f'{url}', '100%', '800px')) p.join() except (KeyboardInterrupt, SystemExit): print('\n! Received keyboard interrupt, stopping server.\n') pass
def update(self): """Update the displayed output and scroll to its end. NOTE: when this widgets is called by ProcessFollowerWidget in non-blocking manner the auto-scrolling won't work. There used to be a function for the Textarea widget, but it didn't work properly and got removed. For more information please visit: https://github.com/jupyter-widgets/ipywidgets/issues/1815""" if self.calculation is None: return try: output_file_path = os.path.join(self.calculation.outputs.remote_folder.get_remote_path(), self.calculation.attributes['output_filename']) except KeyError: self.placeholder = "The `output_filename` attribute is not set for " \ f"{self.calculation.process_class}. Nothing to show." except NotExistentAttributeError: self.placeholder = "The object `remote_folder` was not found among the process outputs. " \ "Nothing to show." else: if os.path.exists(output_file_path): with open(output_file_path) as fobj: difference = fobj.readlines()[len(self.output):-1] # Only adding the difference self.output += difference self.value += ''.join(difference) # Auto scroll down. Doesn't work in detached mode. # Also a hack as it is applied to all the textareas display( Javascript(""" $('textarea').each(function(){ // get the id var id_value = $(this).attr('id'); if (typeof id_value !== 'undefined') { // the variable is defined var textarea = document.getElementById(id_value); textarea.scrollTop = textarea.scrollHeight; } });"""))
def _js_update(self, progress): if progress is None: text = '' elif progress.finished: text = "{} finished in {}.".format( escape(progress.name_after), timestamp2timedelta(progress.elapsed_seconds())) elif progress.max_steps is None: text = ("{task}… duration: {duration}".format( task=escape(progress.name_during), duration=timestamp2timedelta(progress.elapsed_seconds()))) else: text = ("{task}… {progress:.0f}%, ETA: {eta}".format( task=escape(progress.name_during), progress=100. * progress.progress, eta=timestamp2timedelta(progress.eta()))) if progress.max_steps is None: update = self._update_unknown_steps(progress) else: update = self._update_known_steps(progress) if progress.finished: finish = ''' fill.style.animation = 'none'; fill.style.backgroundImage = 'none'; ''' else: finish = '' return Javascript(''' (function () {{ var root = document.getElementById('{uuid}'); var text = root.getElementsByClassName('pb-text')[0]; var fill = root.getElementsByClassName('pb-fill')[0]; text.innerHTML = '{text}'; {update} {finish} }})(); '''.format(uuid=self._uuid, text=text, update=update, finish=finish))
def builder(): """!@brief The function called from the Jupyter notebook to display the widgets Execute the following to display all the widgets in the notebook @code import pnab pnab.builder() @endcode @returns None """ # Prevents auto-scrolling of the notebook disable_js = """ IPython.OutputArea.prototype._should_scroll = function(lines) { return false; } """ display(Javascript(disable_js)) user_input_file(_options_dict)
def run_next_cells(n): if n=='all': n = 'NaN' elif n<1: return js_code = """ var num = {0}; var run = false; var current = $(this)[0]; $.each(IPython.notebook.get_cells(), function (idx, cell) {{ if ((cell.output_area === current) && !run) {{ run = true; }} else if ((cell.cell_type == 'code') && !(num < 1) && run) {{ cell.execute(); num = num - 1; }} }}); """.format(n) display(Javascript(js_code))
def interactive_heatmap(df): """Create an interactive heatmap visualization.""" js_src = resource_string('iventure.jsviz', 'heatmap.js') pivot = df.pivot( index=df.columns[-3], columns=df.columns[-2], values=df.columns[-1], ) pivot.fillna(0, inplace=True) D = pivot.as_matrix() ordering = utils_plot._clustermap_ordering(D) labels = list(np.asarray(pivot.columns)[ordering[0]]) return Javascript( js_src \ + 'heatmap(' \ + df.to_json(orient='split') \ + ',' \ + json.dumps(labels) \ + ')' )
def get_voices(self): """Get a list of supported voices. The voices can be seen in the notebook via the `voicelist` global variable. """ # TO DO - how do we reference the voicelist variable in this class? # via https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/getVoices js = ''' var voices = window.speechSynthesis.getVoices(); var voicelist = ''; for(var i = 0; i < voices.length; i++) { voicelist = voicelist+i+': '+ voices[i].name + ' ('+ voices[i].lang +')'; if(voices[i].default) { voicelist += ' -- DEFAULT'; } voicelist = voicelist + '\\n' } IPython.notebook.kernel.execute('voicelist_raw = """'+ voicelist+'"""; voicelist = voicelist_raw.split("*")'); ''' display(Javascript(js))
def __init__(self): self.comm = None folder = os.path.dirname(__file__) with open(os.path.join(folder, "../js", "initIPython.js"), "r") as fd: initIPython = fd.read() display_javascript(Javascript(initIPython)) css = """ <style> div.output_area img, div.output_area svg { max-width: 100%; height: 100%; } </style> """ display_html(HTML(css)) time.sleep(0.5) self.comm = Comm(target_name='nvd3_stat', data={'event': 'open'})
def update(self, reaction_data=None, metabolite_data=None, gene_data=None): self.gene_data = gene_data if gene_data is not None else self.gene_data self.reaction_data = reaction_data if reaction_data is not None else self.reaction_data self.metabolite_data = metabolite_data if metabolite_data is not None else self.metabolite_data js = Javascript( 'var builder = window.escher_builders["{the_id}"];\n' 'var reaction_data_{the_id} = {reaction_data};\n' 'var metabolite_data_{the_id} = {metabolite_data};\n' 'var gene_data_{the_id} = {gene_data};' 'builder.set_reaction_data(reaction_data_{the_id});' 'builder.set_metabolite_data(metabolite_data_{the_id});' 'builder.set_gene_data(gene_data_{the_id});'.format( the_id=self.the_id, reaction_data=(json.dumps(self.reaction_data) if self.reaction_data else 'null'), metabolite_data=(json.dumps(self.metabolite_data) if self.metabolite_data else 'null'), gene_data=(json.dumps(self.gene_data) if self.gene_data else 'null'))) display(js)
def confirm(self, msg, callback_ok, type='none'): js = ''' $.confirm({ title: '', content: '%s', theme: 'modern', %s buttons: { confirm: function () { %s }, cancel: function () { } } }); ''' % (msg, HGUtil.ALERT_TYPES[type], 'IPython.notebook.kernel.execute("Init.hgmain.' + callback_ok + '")') display(Javascript(js))
def to_jupyter(graph, width=DEFAULT_WIDTH, height=DEFAULT_HEIGHT, color_map=None): """Displays the BEL graph inline in a Jupyter notebook. To use successfully, make run as the last statement in a cell inside a Jupyter notebook. :param pybel.BELGraph graph: A BEL graph :param width: The width of the visualization window to render :type width: int :param height: The height of the visualization window to render :type height: int :param color_map: A dictionary from PyBEL internal node functions to CSS color strings like #FFEE00. Defaults to :data:`default_color_map` :type color_map: dict :return: An IPython notebook Javascript object :rtype: :class:`IPython.display.Javascript` """ return Javascript( to_jupyter_str(graph, width=width, height=height, color_map=color_map))
def generate_app_cell(validated_spec=None, spec_tuple=None): """Produces an invisible blob of JavaScript that inserts a new cell in the notebook, and crams the validated_spec in it. It then removes itself, so it won't happen again on page reload. For the inputs, validated_spec > spec_tuple. That is, if validated_spec is present, that's always used. if spec_tuple is there, and validated_spec is not, then the tuple's used. Also, the tuple should be (spec_json, display_yaml), all as strings. """ if spec_tuple is not None and validated_spec is None: nms = clients.get("narrative_method_store") validated = nms.validate_method({ "id": "some_test_app", "spec_json": spec_tuple[0], "display_yaml": spec_tuple[1] }) if validated.get('is_valid', 0) == 1: validated_spec = validated['method_spec'] elif "errors" in validated and validated['errors']: raise Exception(validated['errors']) js_template = """ var outputArea = this, cellElement = outputArea.element.parents('.cell'), cellIdx = Jupyter.notebook.get_cell_elements().index(cellElement), thisCell = Jupyter.notebook.get_cell(cellIdx), spec_json = '{{spec}}', cellData = { type: 'devapp', appTag: 'dev', appSpec: JSON.parse(spec_json) }; Jupyter.narrative.insertAndSelectCell('code', 'below', cellIdx, cellData); """ js_code = Template(js_template).render(spec=json.dumps(validated_spec)) return Javascript(data=js_code, lib=None, css=None)