Source code for visualization_toolkit.helpers.dash.export
from tempfile import NamedTemporaryFile
import pandas as pd
from dash import Input, Output, State, MATCH, Dash, dcc, no_update
import yipit_dash_mui_components as ymc
from dash_iconify import DashIconify
from visualization_toolkit.helpers.dash.core import get_triggered_id
from visualization_toolkit.helpers.plotly.charts.core.figure import (
generate_pdf_from_figure,
)
class DownloadComponentTypes:
CHART_DOWNLOAD = "chart-download"
CHART_DOWNLOAD_ICON = "chart-download-icon"
CHART_DOWNLOAD_MENU = "chart-download-options"
CSV_FILE = "csv-file"
XLSX_FILE = "xlsx-file"
DOWNLOAD_BUFFER = "download-buffer"
CHART = "chart"
[docs]
def export_chart_button_component(index: str):
"""
Adds a button + dropdown to download a given chart's data. This function is intended to be
used with the ``chart_component`` function.
Export buttons should be placed in the top-right section of a ``ymc.ChartCardHeader``. Typically inside the ``ymc.ChartCartControls``
:param index: Index to set for the subcomponents of the chart button. This should match the index passed into the corresponding ``chart_component``.
:return:
Examples
^^^^^^^^^^^^^
.. code-block:: python
:caption: Example of adding a chart component to a chart card with an export button in the chart controls.
from visualization_toolkit.helpers.dash import setup_chart_exports, ymc
app = Dash(__name__)
# Create a dash app with a chart and an export chart button
app.layout = [
ymc.YipitMUIProvider(
children=ymc.ChartCard([
ymc.ChartCardHeader([
ymc.ChartCartControls([
export_chart_component(index="example-chart"),
]),
]),
chart_component(index="example-chart"),
]),
)
]
# enable export functionality via callbacks
setup_chart_exports(app)
"""
download_container_id = f"{index}-download-container"
return ymc.Box(
id=download_container_id,
sx={"align-self": "center"},
children=[
# Export Button
ymc.Button(
id={"type": DownloadComponentTypes.CHART_DOWNLOAD, "index": index},
variant="text",
children=[
DashIconify(
id={
"type": DownloadComponentTypes.CHART_DOWNLOAD_ICON,
"index": index,
},
icon="material-symbols:download-2",
style={
"height": "1.4375rem",
"width": "1.4375rem",
"color": "#9E9E9E",
"margin": "auto 0",
},
),
],
),
# Export options
ymc.Menu(
id={"type": DownloadComponentTypes.CHART_DOWNLOAD_MENU, "index": index},
anchorEl={"id": download_container_id},
open=False,
children=[
ymc.MenuItem(
"Export as CSV",
{"type": DownloadComponentTypes.CSV_FILE, "index": index},
),
ymc.MenuItem(
"Export as XLSX",
{"type": DownloadComponentTypes.XLSX_FILE, "index": index},
),
],
),
# Download File integration with Dash
dcc.Download(
id={"type": DownloadComponentTypes.DOWNLOAD_BUFFER, "index": index}
),
],
)
[docs]
def setup_chart_exports(app: Dash):
"""
Sets up file exports for each chart in a dash app that is using the standard
``chart_component``. In addition, the dash app must include the ``export_chart_button`` component
for each chart that needs to support exports.
A dropdown will then appear with options to export a chart data as CSV or XLSX files.
:param app:
:return:
Examples
^^^^^^^^^^^^^
.. code-block:: python
:caption: Example of enabling chart downloads for a dash page with a given chart and export button.
from visualization_toolkit.helpers.dash import setup_chart_exports, ymc
app = Dash(__name__)
# Create a dash app with a chart and an export chart button
app.layout = [
ymc.YipitMUIProvider(
children=ymc.Box([
chart_component(index="example-chart"),
export_chart_component(index="example-chart"),
]),
)
]
# enable export functionality via callbacks
setup_chart_exports(app)
"""
@app.callback(
Output(
{"type": DownloadComponentTypes.CHART_DOWNLOAD_MENU, "index": MATCH}, "open"
),
Input(
{"type": DownloadComponentTypes.CHART_DOWNLOAD, "index": MATCH}, "n_clicks"
),
Input({"type": DownloadComponentTypes.CSV_FILE, "index": MATCH}, "n_clicks"),
Input({"type": DownloadComponentTypes.XLSX_FILE, "index": MATCH}, "n_clicks"),
State(
{"type": DownloadComponentTypes.CHART_DOWNLOAD_MENU, "index": MATCH}, "open"
),
prevent_initial_call=True,
)
def toggle_export(
dropdown_clicks,
csv_clicks,
xlsx_clicks,
is_open,
):
parsed_id = get_triggered_id()
if parsed_id["type"] == DownloadComponentTypes.CHART_DOWNLOAD:
if not is_open:
return True
return False
return False
@app.callback(
Output(
{"type": DownloadComponentTypes.DOWNLOAD_BUFFER, "index": MATCH}, "data"
),
Input({"type": DownloadComponentTypes.CSV_FILE, "index": MATCH}, "n_clicks"),
Input({"type": DownloadComponentTypes.XLSX_FILE, "index": MATCH}, "n_clicks"),
State({"type": DownloadComponentTypes.CHART, "index": MATCH}, "figure"),
prevent_initial_call=True,
)
def process_export(
csv_clicks,
xlsx_clicks,
figure,
):
parsed_id = get_triggered_id()
pdf = generate_pdf_from_figure(figure)
match parsed_id["type"]:
case DownloadComponentTypes.CSV_FILE:
with NamedTemporaryFile("wb") as f:
pdf.to_csv(f.name, index=False)
f.seek(0)
return dcc.send_file(f.name, filename=f"export.csv")
case DownloadComponentTypes.XLSX_FILE:
with NamedTemporaryFile("wb") as f:
with pd.ExcelWriter(f.name, engine="xlsxwriter") as writer:
pdf.to_excel(writer, sheet_name="export", index=False)
f.seek(0)
return dcc.send_file(f.name, filename="export.xlsx")
return no_update