-
-
Save exzhawk/33e5dcfc8859e3b6ff4e5269b1ba0ba4 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*- | |
import os | |
from html.parser import HTMLParser | |
import dash | |
import pandas as pd | |
import plotly.express as px | |
import requests | |
from dash import html, dcc, dash_table, Input, Output | |
def patch_file(file_path: str, content: bytes, extra: dict = None) -> bytes: | |
if file_path == 'index.html': | |
index_html_content = content.decode('utf8') | |
extra_jsons = f''' | |
var patched_jsons_content={{ | |
{','.join(["'/" + k + "':" + v.decode("utf8") + "" for k, v in extra.items()])} | |
}}; | |
''' | |
patched_content = index_html_content.replace( | |
'<footer>', | |
f''' | |
<footer> | |
<script> | |
''' + extra_jsons + ''' | |
const origFetch = window.fetch; | |
window.fetch = function () { | |
const e = arguments[0] | |
if (patched_jsons_content.hasOwnProperty(e)) { | |
return Promise.resolve({ | |
json: () => Promise.resolve(patched_jsons_content[e]), | |
headers: new Headers({'content-type': 'application/json'}), | |
status: 200, | |
}); | |
} else { | |
return origFetch.apply(this, arguments) | |
} | |
} | |
</script> | |
''' | |
).replace( | |
'href="/', | |
'href="' | |
).replace( | |
'src="/', | |
'src="' | |
) | |
return patched_content.encode('utf8') | |
else: | |
return content | |
def write_file(file_path: str, content: bytes, target_dir='target', ): | |
target_file_path = os.path.join(target_dir, file_path.lstrip('/').split('?')[0]) | |
target_leaf_dir = os.path.dirname(target_file_path) | |
os.makedirs(target_leaf_dir, exist_ok=True) | |
with open(target_file_path, 'wb') as f: | |
f.write(content) | |
pass | |
class ExternalResourceParser(HTMLParser): | |
def __init__(self): | |
super().__init__() | |
self.resources = [] | |
def handle_starttag(self, tag, attrs): | |
if tag == 'link': | |
for k, v in attrs: | |
if k == 'href': | |
self.resources.append(v) | |
if tag == 'script': | |
for k, v in attrs: | |
if k == 'src': | |
self.resources.append(v) | |
def make_static(base_url, target_dir='target'): | |
index_html_bytes = requests.get(base_url).content | |
json_paths = ['_dash-layout', '_dash-dependencies', ] | |
extra_json = {} | |
for json_path in json_paths: | |
json_content = requests.get(base_url + json_path).content | |
extra_json[json_path] = json_content | |
patched_bytes = patch_file('index.html', index_html_bytes, extra=extra_json) | |
write_file('index.html', patched_bytes, target_dir) | |
parser = ExternalResourceParser() | |
parser.feed(patched_bytes.decode('utf8')) | |
extra_js = [ | |
'_dash-component-suites/dash/dcc/async-graph.js', | |
'_dash-component-suites/dash/dcc/async-plotlyjs.js', | |
'_dash-component-suites/dash/dash_table/async-table.js', | |
'_dash-component-suites/dash/dash_table/async-highlight.js' | |
] | |
for resource_url in parser.resources + extra_js: | |
resource_url_full = base_url + resource_url | |
print(f'get {resource_url_full}') | |
resource_bytes = requests.get(resource_url_full).content | |
patched_bytes = patch_file(resource_url, resource_bytes) | |
write_file(resource_url, patched_bytes, target_dir) | |
def main(): | |
port = 9050 | |
app = dash.Dash(__name__) | |
df = pd.DataFrame({ | |
"Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"], | |
"Amount": [4, 1, 2, 2, 4, 5], | |
"City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"] | |
}) | |
fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group") | |
app.layout = html.Div(children=[ | |
html.Button('save static', id='save', n_clicks=0), | |
html.Span('', id='saved'), | |
html.H1(children='Hello Dash'), | |
html.Div(children=''' | |
Dash: A web application framework for your data. | |
'''), | |
dcc.Graph( | |
id='example-graph', | |
figure=fig | |
), | |
dash_table.DataTable( | |
id='table', | |
columns=[{"name": i, "id": i} for i in df.columns], | |
data=df.to_dict('records'), | |
) | |
]) | |
@app.callback( | |
Output('saved', 'children'), | |
Input('save', 'n_clicks'), | |
) | |
def save_result(n_clicks): | |
if n_clicks == 0: | |
return 'not saved' | |
else: | |
make_static(f'http://127.0.0.1:{port}/') | |
return 'saved' | |
app.run_server(debug=False, port=port) | |
if __name__ == '__main__': | |
main() |
Hi,
Just to clarify, if I make a callback with this code it ain't possible to save it as the current layout?, I think you were really specific about it, but I'd like to know why it won't do it.
it ain't possible to save it as the current layout?
After opening the page, Dash serves the initial layout. When a callback is invoked (your click, input, etc), Dash then only transfers differential data to patch the layout. The code above opens a new page and saves it. It can not invoke callbacks (mimic your click, input, etc.), save differential data or the patched layout. That's why it won't work with callbacks and current layout.
Technically it is possible, by invoking callbacks or directly dumping current layout from the frontend, but I didn't think it worthwhile to dig that.
Thanks for your answer!
Hey,
Great Post Man!!!
Is it possible to save to html directly (line135, After defining the layout)
Hi, exzhawk
I have run your code in vscode, and click the "save" button in the server, then create a 'target' folder normally. But there is no figure graph in the 'index.html', only the table and other divs in the page.
There is the console log in html page. and an error in vscode terminal. Could you help me with this issue? Thanks.
Hi. Very nice snippet!!!
For those encountering the issue mentioned by @RajonDawn, just change these:
extra_js = [
"_dash-component-suites/dash/dcc/async-graph.js",
# "_dash-component-suites/dash/dcc/async-plotlyjs.js", # TODO: remove
"_dash-component-suites/dash/dash_table/async-table.js",
"_dash-component-suites/dash/dash_table/async-highlight.js",
"_dash-component-suites/plotly/package_data/plotly.min.js", # TODO: add
]
Just want to say a big thank you for creating this snippet 3 years ago, really nice snippet, saved me from tons of time and frustration.
This script shows how to make a Dash layout fully offline.
To see the demo:
Download the Python file and run it. You will see a simple bar chart as well as a table. After clicking the left upper button, you will find a folder besides the Python file named "target". In which you will have a fully offline static HTML file with all other files needed (hopefully). You may stop Python and open the index.html directly inside the "target" folder. You may send the whole "target" folder to others and let them open it without any Python runtime or HTTP server, just with a browser.
Under the hood:
The code is actually simple but hacky. The dash web page relies on ajax communication to fetch JSON data to render. So the make_static function downloads all resources and the JSON, patches the JSON into the index.html file, and tells the scripts in the page to get data from index.html instead of requesting to Python backend.
Limitation:
It's a static HTML file bro. No fancy callbacks anymore. You can only save the initial state rather than the ideal "current layout when the save button is clicked".