From 32da210784e88fefa4fac614b9108171277e6d61 Mon Sep 17 00:00:00 2001 From: Marco Zocca Date: Tue, 9 Jan 2024 00:11:55 +0100 Subject: [PATCH] v 0.3 --- CHANGELOG.md | 9 +++++++++ README.md | 8 +++++--- htmx-plotly.js | 14 +++++++++++--- index.html | 5 +++-- plotly_utils.py | 10 ++++++++-- server.py | 12 ++++-------- 6 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..681d550 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# 0.3 + +* the HTMX swap mechanism works once more as expected: this extension now receives an *object* from the server, which +is unpacked into Plotly restyle data and HTML markup. + +# 0.2 + +* use [`plotly_utils.py`](https://cdn.jsdelivr.net/gh/ocramz/htmx-plotly@0.2/plotly_utils.py) to convert between +Plotly objects and restyle-friendly JSON. diff --git a/README.md b/README.md index ade109b..82d82f3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Possible applications include: simple dashboards, data apps, and similar. Load the script from CDN into the head of your HTML file: ```html -``` + ``` and of course also HTMX and Plotly: @@ -26,15 +26,17 @@ and of course also HTMX and Plotly: Add these attributes to a page element: * `hx-ext="htmx-plotly"` means this element uses the extension * `hx-post="/get-data"` the HTTP endpoint that returns the new plot data -* `hx-swap="none"` don't mutate the DOM with the result (we need to call the Plotly API instead) * `plot-id="my-plot"` ID of the DOM element that hosts the Plotly chart. Example: here we make an `` text link trigger the update of the plot within element `my-plot`: ```html -

UPDATE

+

UPDATE

``` +NB: As of `v0.3` the HTMX swap mechanism works once more as expected: this extension now receives an *object* from the server, which +is unpacked into Plotly restyle data and HTML markup. + ### Setup (frontend) Plotly charts need an empty div element as well as a script tag for initialization: diff --git a/htmx-plotly.js b/htmx-plotly.js index d84c8bd..808b1e6 100644 --- a/htmx-plotly.js +++ b/htmx-plotly.js @@ -1,5 +1,11 @@ // adapted from https://unpkg.com/htmx.org@1.9.10/dist/ext/client-side-templates.js +function htmlUnescape(input) { + // // HTML-in-JSON should be escaped on the wire, and decoded safely here with DOMParser to avoid XSS + var doc = new DOMParser().parseFromString(input, "text/html"); + return doc.documentElement.textContent +} + htmx.defineExtension('htmx-plotly', { transformResponse : (text, xhr, elt) => { @@ -7,9 +13,11 @@ htmx.defineExtension('htmx-plotly', { const pedantic = false var plotEl = htmx.closest(elt, "[plot-id]"); // closest including div element + const payload = JSON.parse(text) + const dataNew = payload['restyle_data'] + n = dataNew.length + const markup = htmlUnescape(payload['markup']) // to be passed back to HTMX for swapping if (plotEl) { - const dataNew = JSON.parse(text) - n = dataNew.length const plotId = plotEl.getAttribute('plot-id'); // lookup value of ? in < .. plot-id="?"> var plotDiv = htmx.find("#" + plotId); // div element pointed at if (plotDiv) { @@ -31,7 +39,7 @@ htmx.defineExtension('htmx-plotly', { console.log('No plot-id attribute defined') } - return '' + return markup } } diff --git a/index.html b/index.html index 76565ef..32ca3a6 100644 --- a/index.html +++ b/index.html @@ -10,15 +10,16 @@ -
+
+
+
diff --git a/plotly_utils.py b/plotly_utils.py index f9c4f70..43e74ff 100644 --- a/plotly_utils.py +++ b/plotly_utils.py @@ -1,8 +1,10 @@ import json +from markupsafe import Markup, escape -def plotlyToRestyle(x): +def plotlyToRestyle(x, htmlStr): """ :param x: a Plotly object with a .to_json() implem, e.g. a Figure + :param htmlStr: HTML string that will be passed to HTMX for swapping :return: data that can be passed to restyle in Plotly.js """ z = x.to_json() # .to_json() is Plotly implem @@ -10,4 +12,8 @@ def duplicate(w): w['x'] = [w['x']] # the .restyle() nested array bs w['y'] = [w['y']] return w - return [duplicate(w) for w in json.loads(z)['data']] \ No newline at end of file + obj = { + 'restyle_data': [duplicate(w) for w in json.loads(z)['data']], + 'markup': str(escape(htmlStr)) + } + return obj diff --git a/server.py b/server.py index 7490986..57c8bce 100644 --- a/server.py +++ b/server.py @@ -1,8 +1,8 @@ from flask import Flask, request, send_file, make_response -import json -from markupsafe import Markup, escape -import jsonpickle +# import json +# from markupsafe import Markup, escape +# import jsonpickle from plotly_utils import plotlyToRestyle from plotly_compound_scatter_test import irisScatter1 @@ -19,11 +19,7 @@ def getData(): # return f'Hello, {escape(name)}!' x = irisScatter1() - # z = x.to_json() # .to_json() is Plotly implem - # w = json.loads(z)['data'][0] # parse back into dict and extract data - # w['x'] = [w['x']] # the .restyle() nested array bs - # w['y'] = [w['y']] - w = plotlyToRestyle(x) + w = plotlyToRestyle(x, 'It wOrKs') print(f'.restyle data: {w}') return make_response(w)