July 26, 2023
Dash.jl is the Julia version of Dash from Plotly, an open-source framework for building data science web applications.
Dash.jl is more robust than the other web UI libraries in the Julia ecosystem
I’m currently working for R2, engineering consultants in the chlorine production industry, where I split my time between
v1.3.0
)DashExtensions.jl
, a package with more experimental features + Julian syntax?starting from a simple app
using Dash
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
run_server(app; debug=true)
start with using Dash
and instantiate a DashApp
type instance
using Dash
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
run_server(app; debug=true)
setup the layout
using Dash
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
run_server(app; debug=true)
setup a callback (i.e. a UI -> Julia -> UI interaction handler)
using Dash
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
run_server(app; debug=true)
module Utils
using CSV, DataFrames, Dates
const PATH_DATA = joinpath(@__DIR__, "data")
load_df(fname) = DataFrame(CSV.File(joinpath(PATH_DATA, fname)))
function make_gantt(df::AbstractDataFrame)
@assert names(df) == ["Name", "Start", "End"]
@assert eltype(df.Start) == DateTime
@assert eltype(df.End) == DateTime
# `x` correspond to the span of the bar
y = df.Name
base = df.Start
x = (df.End .- df.Start) .|> Dates.value
# show "raw" `Start` and `End` values in hover labels
customdata = collect(zip(df.Start, df.End))
hovertemplate = "<b>Start:</b> %{customdata[0]}<br>" *
"<b>End:</b> %{customdata[1]}" *
"<extra>%{y}</extra>"
# N.B. `type="date"` so that `x` Int get interpreted as time durations
xaxis = (type="date",)
return (data = [(; type="bar", orientation="h",
y, base, x, customdata, hovertemplate)],
layout = (; title="Gantt chart",
xaxis))
end
make_gantt(fname::AbstractString) = make_gantt(load_df(fname))
end # module Utils
from dash import Dash, html, dcc, callback, Output, Input
import os
from _utils import PATH_DATA, make_gantt
app = Dash(external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html.Div([
html.H1("Dash at JuliaCon 2023"),
dcc.Dropdown(id="my-dropdown",
options=[{"label": os.path.splitext(fname)[0], "value": fname}
for fname in os.listdir(PATH_DATA)],
placeholder="Please select a data file",
style={"width": "400px"}),
dcc.Graph(id="my-graph")
])
@callback(Output("my-graph", "figure"),
Input("my-dropdown", "value"))
def update_graph(fname):
return {} if fname is None else make_gantt(fname)
app.run(debug=True)
a good place to look for property names
julia> using Dash
help?> dcc_graph
search: dcc_graph
dcc_graph(;kwargs...)
A Graph component Graph can be used to render any plotly.js-powered data visualization.
You can define callbacks based on user interaction with Graphs such as hovering, clicking or selecting
• id (String; optional): The ID of this component, used to identify dash components
in callbacks. The ID needs to be unique across all of the components in an app.
• animate (Bool; optional): Beta: If true, animate between updates using
plotly.js's animate function
• animation_options (Dict; optional): Beta: Object containing animation settings.
Only applies if animate is true
• className (String; optional): className of the parent div
• clear_on_unhover (Bool; optional): If True, clear_on_unhover will clear the hoverData property
when the user "unhovers" from a point. If False, then the hoverData property will be equal to the data from the last point that was hovered over.
• clickAnnotationData (Dict; optional): Data from latest click annotation event. Read-only.
• clickData (Dict; optional): Data from latest click event. Read-only.
• config (optional):Plotly.js config options.
See https://plotly.com/javascript/configuration-options/ for more info.. config has the following type: lists containing elements staticPlot, plotlyServerURL, editable, editSelection, edits, autosizable,
responsive, queueLength, fillFrame, frameMargins, scrollZoom, doubleClick, doubleClickDelay, showTips, showAxisDragHandles, showAxisRangeEntryBoxes, showLink, sendData, linkText, displayModeBar,
showSendToCloud, showEditInChartStudio, modeBarButtonsToRemove, modeBarButtonsToAdd, modeBarButtons, toImageButtonOptions, displaylogo, watermark, plotGlPixelRatio, topojsonURL, mapboxAccessToken, locale,
locales - staticPlot (Bool; optional): No interactivity, for export or image generation - plotlyServerURL (String; optional): Base URL for a Plotly cloud instance, if showSendToCloud is enabled - editable
(Bool; optional): We can edit titles, move annotations, etc - sets all pieces of edits unless a separate edits config item overrides individual parts - editSelection (Bool; optional): Enables moving selections
- edits (optional):A set of editable properties. edits has the following type: lists containing elements annotationPosition, annotationTail, annotationText, axisTitleText, colorbarPosition, colorbarTitleText,
legendPosition, legendText, shapePosition, titleText - annotationPosition (Bool; optional): The main anchor of the annotation, which is the text (if no arrow) or the arrow (which drags the whole thing leaving
the arrow length & direction unchanged) - annotationTail (Bool; optional): Just for annotations with arrows, change the length and direction of the arrow - annotationText (Bool; optional) - axisTitleText
(Bool; optional) - colorbarPosition (Bool; optional) - colorbarTitleText (Bool; optional) - legendPosition (Bool; optional) - legendText (Bool; optional): Edit the trace name fields from the legend -
shapePosition (Bool; optional) - titleText (Bool; optional): The global layout.title - autosizable (Bool; optional): DO autosize once regardless of layout.autosize (use default width or height values
otherwise) - responsive (Bool; optional): Whether to change layout size when the window size changes - queueLength (optional): Set the length of the undo/redo queue - fillFrame (Bool; optional): If we DO
autosize, do we fill the container or the screen? - frameMargins (optional): If we DO autosize, set the frame margins in percents of plot size - scrollZoom (Bool; optional): Mousewheel or two-finger scroll
zooms the plot - doubleClick (false, 'reset', 'autosize', 'reset+autosize'; optional): Double click interaction (false, 'reset', 'autosize' or 'reset+autosize') - doubleClickDelay (optional): Delay for
registering a double-click event in ms. The minimum value is 100 and the maximum value is 1000. By default this is 300. - showTips (Bool; optional): New users see some hints about interactivity -
showAxisDragHandles (Bool; optional): Enable axis pan/zoom drag handles - showAxisRangeEntryBoxes (Bool; optional): Enable direct range entry at the pan/zoom drag points (drag handles must be enabled above) -
showLink (Bool; optional): Link to open this plot in plotly - sendData (Bool; optional): If we show a link, does it contain data or just link to a plotly file? - linkText (String; optional): Text appearing in
the sendData link - displayModeBar (true, false, 'hover'; optional): Display the mode bar (true, false, or 'hover') - showSendToCloud (Bool; optional): Should we include a modebar button to send this data to a
Plotly Cloud instance, linked by plotlyServerURL. By default this is false. - showEditInChartStudio (Bool; optional): Should we show a modebar button to send this data to a Plotly Chart Studio plot. If both
this and showSendToCloud are selected, only showEditInChartStudio will be honored. By default this is false. - modeBarButtonsToRemove (Array; optional): Remove mode bar button by name. All modebar button names
at https://github.com/plotly/plotly.js/blob/master/src/components/modebar/buttons.js Common names include: sendDataToCloud; (2D) zoom2d, pan2d, select2d, lasso2d, zoomIn2d, zoomOut2d, autoScale2d,
resetScale2d; (Cartesian) hoverClosestCartesian, hoverCompareCartesian; (3D) zoom3d, pan3d, orbitRotation, tableRotation, handleDrag3d, resetCameraDefault3d, resetCameraLastSave3d, hoverClosest3d; (Geo)
zoomInGeo, zoomOutGeo, resetGeo, hoverClosestGeo; hoverClosestGl2d, hoverClosestPie, toggleHover, resetViews. - modeBarButtonsToAdd (Array; optional): Add mode bar button using config objects - modeBarButtons
(Bool | Real | String | Dict | Array; optional): Fully custom mode bar buttons as nested array, where the outer arrays represents button groups, and the inner arrays have buttons config objects or names of
default buttons - toImageButtonOptions (optional):Modifications to how the toImage modebar button works. toImageButtonOptions has the following type: lists containing elements format, filename, width, height,
scale - format ('jpeg', 'png', 'webp', 'svg'; optional): The file format to create - filename (String; optional): The name given to the downloaded file - width (optional): Width of the downloaded file, in px -
height (optional): Height of the downloaded file, in px - scale (optional): Extra resolution to give the file after rendering it with the given width and height - displaylogo (Bool; optional): Add the plotly
logo on the end of the mode bar - watermark (Bool; optional): Add the plotly logo even with no modebar - plotGlPixelRatio (optional): Increase the pixel ratio for Gl plot images - topojsonURL (String;
optional): URL to topojson files used in geo charts - mapboxAccessToken (Bool | Real | String | Dict | Array; optional): Mapbox access token (required to plot mapbox trace types) If using an Mapbox Atlas
server, set this option to '', so that plotly.js won't attempt to authenticate to the public Mapbox server. - locale (String; optional): The locale to use. Locales may be provided with the plot (locales below)
or by loading them on the page, see: https://github.com/plotly/plotly.js/blob/master/dist/README.md#to-include-localization - locales (Dict; optional): Localization definitions, if you choose to provide them
with the plot rather than registering them globally.
• extendData (Array | Dict; optional): Data that should be appended to existing traces. Has the form
[updateData, traceIndices, maxPoints], where updateData is an object containing the data to extend, traceIndices (optional) is an array of trace indices that should be extended, and maxPoints (optional) is
either an integer defining the maximum number of points allowed or an object with key:value pairs matching updateData Reference the Plotly.extendTraces API for full usage:
https://plotly.com/javascript/plotlyjs-function-reference/#plotlyextendtraces
• figure (lists containing elements data, layout, frames - data (Array of Dicts; optional) - layout (Dict; optional) - frames (Array of Dicts; optional); optional): Plotly figure object. See schema:
https://plotly.com/javascript/reference
config is set separately by the config property
• hoverData (Dict; optional): Data from latest hover event. Read-only.
• loading_state (lists containing elements isloading, propname, componentname - `isloading(Bool; optional): Determines if the component is loading or not -propname(String; optional): Holds which
property is loading -componentname` (String; optional): Holds the name of the component that is loading; optional): Object that holds the loading state object coming from dash-renderer
• mathjax (Bool; optional): If true, loads mathjax v3 (tex-svg) into the page and use it in the graph
• prependData (Array | Dict; optional): Data that should be prepended to existing traces. Has the form
[updateData, traceIndices, maxPoints], where updateData is an object containing the data to prepend, traceIndices (optional) is an array of trace indices that should be prepended, and maxPoints (optional) is
either an integer defining the maximum number of points allowed or an object with key:value pairs matching updateData Reference the Plotly.prependTraces API for full usage:
https://plotly.com/javascript/plotlyjs-function-reference/#plotlyprependtraces
• relayoutData (Dict; optional): Data from latest relayout event which occurs
when the user zooms or pans on the plot or other layout-level edits. Has the form {<attr string>: <value>} describing the changes made. Read-only.
• responsive (true, false, 'auto'; optional): If True, the Plotly.js plot will be fully responsive to window resize
and parent element resize event. This is achieved by overriding config.responsive to True, figure.layout.autosize to True and unsetting figure.layout.height and figure.layout.width. If False, the Plotly.js
plot not be responsive to window resize and parent element resize event. This is achieved by overriding config.responsive to False and figure.layout.autosize to False. If 'auto' (default), the Graph will
determine if the Plotly.js plot can be made fully responsive (True) or not (False) based on the values in config.responsive, figure.layout.autosize, figure.layout.height, figure.layout.width. This is the
legacy behavior of the Graph component.
Needs to be combined with appropriate dimension / styling through the style prop to fully take effect.
• restyleData (Array; optional): Data from latest restyle event which occurs
when the user toggles a legend item, changes parcoords selections, or other trace-level edits. Has the form [edits, indices], where edits is an object {<attr string>: <value>} describing the changes made, and
indices is an array of trace indices that were edited. Read-only.
• selectedData (Dict; optional): Data from latest select event. Read-only.
• style (Dict; optional): Generic style overrides on the plot div
add callback triggered when user clicks on the Gantt
using Dash
using JSON3
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
callback!(app,
Output("log", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value")) do clickdata, fname
return string((; clickdata, fname))
end
run_server(app; debug=true)
use click data to create a closeup dcc_graph
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
pass a named function (useful for unit testing) to callback!
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
handle not-set callback values using multiple-dispatch
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
handle not-set callback values using multiple-dispatch
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
handle not-set callback values using multiple-dispatch
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
callbacks often have validation steps
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do fname
return isnothing(fname) ? NamedTuple() : make_gantt(fname)
end
function update_closeup(clickdata::JSON3.Object, fname::AbstractString)
log = string((; clickdata, fname))
customdata = try
clickdata.points[1].customdata
catch err
[]
end
if isempty(customdata)
return update_closeup(nothing)
end
pt_start, pt_end = DateTime.(customdata)
range = [pt_start-Day(1), pt_end+Day(1)]
(; data, layout) = make_gantt(fname)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value"))
run_server(app; debug=true)
consider callback value types and a from_dash
function
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
abstract type AbstractCallbackValue end
from_dash(::Type{<:AbstractCallbackValue}, ::Any) = nothing
struct FName <: AbstractCallbackValue
value::String
end
from_dash(T::Type{<:FName}, value::AbstractString) = FName(String(value))
struct ClickedPoint <: AbstractCallbackValue
pt_start::DateTime
pt_end::DateTime
end
function from_dash(T::Type{<:ClickedPoint}, clickdata::JSON3.Object)
# N.B. use try-catch for simplicity
return try
pt_start, pt_end = DateTime.(clickdata.points[1].customdata)
ClickedPoint(pt_start, pt_end)
catch err
nothing
end
end
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
update_figure(fname::FName) = make_gantt(fname.value)
update_figure(args...) = NamedTuple()
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do args...
return update_figure(from_dash(FName, args[1]))
end
function update_closeup(pt::ClickedPoint, fname::FName)
log = string((; pt, fname))
range = [pt.pt_start-Day(1), pt.pt_end+Day(1)]
(; data, layout) = make_gantt(fname.value)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value")) do args...
return update_closeup(from_dash(ClickedPoint, args[1]),
from_dash(FName, args[2]))
end
run_server(app; debug=true)
callback declaration, validation and update logic are now split nicely
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
abstract type AbstractCallbackValue end
from_dash(::Type{<:AbstractCallbackValue}, ::Any) = nothing
struct FName <: AbstractCallbackValue
value::String
end
from_dash(T::Type{<:FName}, value::AbstractString) = FName(String(value))
struct ClickedPoint <: AbstractCallbackValue
pt_start::DateTime
pt_end::DateTime
end
function from_dash(T::Type{<:ClickedPoint}, clickdata::JSON3.Object)
# N.B. use try-catch for simplicity
return try
pt_start, pt_end = DateTime.(clickdata.points[1].customdata)
ClickedPoint(pt_start, pt_end)
catch err
nothing
end
end
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
update_figure(fname::FName) = make_gantt(fname.value)
update_figure(args...) = NamedTuple()
callback!(app,
Output("my-graph", "figure"),
Input("my-dropdown", "value")) do args...
return update_figure(from_dash(FName, args[1]))
end
function update_closeup(pt::ClickedPoint, fname::FName)
log = string((; pt, fname))
range = [pt.pt_start-Day(1), pt.pt_end+Day(1)]
(; data, layout) = make_gantt(fname.value)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
callback!(app,
Output("log", "children"),
Output("closeup", "children"),
Input("my-graph", "clickData"),
State("my-dropdown", "value")) do args...
return update_closeup(from_dash(ClickedPoint, args[1]),
from_dash(FName, args[2]))
end
run_server(app; debug=true)
easy to write a more Julian add_callback
function where we declare the Julia types associated with the callback values
using Dash
using JSON3
using Dates
include("_Utils.jl"); using .Utils: PATH_DATA, make_gantt
abstract type AbstractCallbackValue end
from_dash(::Type{<:AbstractCallbackValue}, ::Any) = nothing
struct FName <: AbstractCallbackValue
value::String
end
from_dash(T::Type{<:FName}, value::AbstractString) = FName(String(value))
struct ClickedPoint <: AbstractCallbackValue
pt_start::DateTime
pt_end::DateTime
end
function from_dash(T::Type{<:ClickedPoint}, clickdata::JSON3.Object)
# N.B. use try-catch for simplicity
return try
pt_start, pt_end = DateTime.(clickdata.points[1].customdata)
ClickedPoint(pt_start, pt_end)
catch err
nothing
end
end
abstract type AbstractDepJl{T} end
from_dash(::AbstractDepJl{T}, val) where {T} = from_dash(T, val)
struct Input_jl{T} <: AbstractDepJl{T}
dep::Dash.Dependency
end
Input_jl(T, compid, prop) = Input_jl{T}(Input(compid, prop))
struct State_jl{T} <: AbstractDepJl{T}
dep::Dash.Dependency
end
State_jl(T, compid, prop) = State_jl{T}(State(compid, prop))
_stubdep(depjl::AbstractDepJl{T}) where {T} = depjl.dep
_stubdep(dep::Dash.Dependency) = dep
_split_deps(T, deps) = collect(filter(x -> isa(x, T), deps))
function add_callback!(func::Function, app::Dash.DashApp, args...; kwargs...)
outputs = _split_deps(Output, args)
inputs = _split_deps(Union{Input, Input_jl}, args)
states = _split_deps(Union{State, State_jl}, args)
inputs2 = _stubdep.(inputs)
states2 = _stubdep.(states)
args2 = vcat(outputs, inputs2, states2)
func2(vals...) = begin
vals2 = map(enumerate(vals)) do (i, val)
dep = vcat(inputs, states)[i]
if isa(dep, AbstractDepJl)
from_dash(dep, val)
else
val
end
end
return func(vals2...)
end
return callback!(func2, app, args2...; kwargs...)
end
app = dash(; external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
app.layout = html_div() do
html_h1("Dash.jl at JuliaCon 2023"),
dcc_dropdown(; id="my-dropdown",
options=[(label=first(splitext(fname)), value=fname)
for fname in readdir(PATH_DATA)],
placeholder="Please select a data file",
style=(; width="400px")),
dcc_graph(; id="my-graph"),
html_details() do
html_summary("Click Data"),
html_pre(; id="log")
end,
html_div(; id="closeup")
end
update_figure(fname::FName) = make_gantt(fname.value)
update_figure(::Nothing) = NamedTuple()
add_callback!(update_figure,
app,
Output("my-graph", "figure"),
Input_jl(FName, "my-dropdown", "value"))
function update_closeup(pt::ClickedPoint, fname::FName)
log = string((; pt, fname))
range = [pt.pt_start-Day(1), pt.pt_end+Day(1)]
(; data, layout) = make_gantt(fname.value)
layout2 = (; layout...,
title="Close up!",
xaxis=(; layout.xaxis..., range))
figure = (; data, layout=layout2)
return log, dcc_graph(; figure)
end
update_closeup(args...) = "empty!", nothing
add_callback!(update_closeup,
app,
Output("log", "children"),
Output("closeup", "children"),
Input_jl(ClickedPoint, "my-graph", "clickData"),
State_jl(FName, "my-dropdown", "value"))
run_server(app; debug=true)
quarto project available at:
https://gitlab.com/etpinard/dash.jl-tricks/-/tree/main/slides/juliacon2023
Étienne Tétreault-Pinard