diff --git a/scripts/dash_plot.jl b/scripts/dash_plot.jl
new file mode 100644
index 00000000..89c017f0
--- /dev/null
+++ b/scripts/dash_plot.jl
@@ -0,0 +1,714 @@
+import Dash
+import Plotly
+import PlotlyJS
+import DataFrames
+import Statistics
+import Sockets
+import HTTP
+
+global dash_df
+global dash_server
+global dash_task
+global dash_stop = false
+
+function load_dataframes(gdata::Data; execute::Bool=false, edges::Bool=false)
+ # Source data
+ if isdefined(gdata, :sources)
+ nsources = length(gdata.sources)
+ lat = Vector{Float64}(undef, nsources)
+ lon = Vector{Float64}(undef, nsources)
+ label = Vector{String}(undef, nsources)
+ name = Vector{String}(undef, nsources)
+ id = Vector{String}(undef, nsources)
+ for i = 1:nsources
+ lat[i] = gdata.sources[i].lat
+ lon[i] = gdata.sources[i].lon
+ label[i] = string(gdata.sources[i].label)
+ name[i] = "Source $i"
+ id[i] = string(gdata.sources[i].label)
+ end
+ df_source = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ else
+ @warn("Source data is missing!")
+ df_source = DataFrames.DataFrame()
+ end
+ # Sink data
+ if isdefined(gdata, :sinks)
+ nsinks = length(gdata.sinks)
+ lat = Vector{Float64}(undef, nsinks)
+ lon = Vector{Float64}(undef, nsinks)
+ label = Vector{String}(undef, nsinks)
+ name = Vector{String}(undef, nsinks)
+ id = Vector{String}(undef, nsinks)
+ for i = 1:nsinks
+ lat[i] = gdata.sinks[i].lat
+ lon[i] = gdata.sinks[i].lon
+ label[i] = string(gdata.sinks[i].label)
+ name[i] = "Sink $i"
+ id[i] = string(gdata.sinks[i].label)
+ end
+ df_sink = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ else
+ @warn("Sink data is missing!")
+ df_sink = DataFrames.DataFrame()
+ end
+ # Candidate network
+ if !isdefined(gdata, :graphEdgeRoutes) && execute
+ @warn("Candidate network data is missing! Generation of the candidate network will be executed ...")
+ generateCandidateNetwork!(gdata)
+ end
+ if isdefined(gdata, :graphEdgeRoutes)
+ label = Vector{String}(undef, 0)
+ name = Vector{String}(undef, 0)
+ id = Vector{String}(undef, 0)
+ if edges
+ lat = Vector{Vector{Float64}}(undef, 0)
+ lon = Vector{Vector{Float64}}(undef, 0)
+ i = 1
+ for (key, route) in gdata.graphEdgeRoutes
+ latv = Vector{Float64}(undef, 0)
+ lonv = Vector{Float64}(undef, 0)
+ for cellNum in route
+ lon_temp, lat_temp = cellToLocation(gdata, cellNum)
+ push!(latv, lat_temp)
+ push!(lonv, lon_temp)
+ end
+ if gdata.projectionVersion == 2
+ coordinates = AEACPXYtoLatLon(lonv, latv)
+ push!(lat, coordinates[:,1])
+ push!(lon, coordinates[:,2])
+ else
+ push!(lat, latv)
+ push!(lon, lonv)
+ end
+ push!(label, "$(i)")
+ push!(name, "$(key)")
+ push!(id, "$(i)")
+ i += 1
+ end
+ else
+ lat = Vector{Union{Missing,Float64}}(undef, 0)
+ lon = Vector{Union{Missing,Float64}}(undef, 0)
+ if gdata.projectionVersion == 2
+ i = 1
+ for (key, route) in gdata.graphEdgeRoutes
+ x = Vector{Float64}(undef, 0)
+ y = Vector{Float64}(undef, 0)
+ for cellNum in route
+ x_temp, y_temp = cellToLocation(gdata, cellNum)
+ push!(x, x_temp)
+ push!(y, y_temp)
+ end
+ coordinates = AEACPXYtoLatLon(x, y)
+ for v in coordinates[:,1]
+ push!(lat, v)
+ end
+ for v in coordinates[:,2]
+ push!(lon, v)
+ push!(label, "$(i)")
+ push!(name, "$(key)")
+ push!(id, "$(i)")
+ end
+ # separate edges with missing values
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(id, "")
+ push!(name, "")
+ i += 1
+ end
+ else
+ i = 1
+ for (key, route) in gdata.graphEdgeRoutes
+ for cellNum in route
+ lon_temp, lat_temp = cellToLocation(gdata, cellNum) # convert cellNum to lat, lon
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "$(i)")
+ push!(name, "$(key)")
+ push!(id, "$(i)")
+ end
+ # separate edges with missing values
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(name, "")
+ push!(id, "")
+ i += 1
+ end
+ end
+ end
+ df_network = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ else
+ @warn("Candidate network data is missing!")
+ df_network = DataFrames.DataFrame()
+ end
+
+ # Delaunay Pairs
+ if !isdefined(gdata, :delaunayPairs) && execute
+ @warn("Delaunay network data is missing! Generation of the Delaunay network will be executed ...")
+ gdata.delaunayPairs = generateDelaunayNetwork(gdata, sourceSinkToCells(gdata))
+ end
+ if isdefined(gdata, :delaunayPairs)
+ label = Vector{String}(undef, 0)
+ name = Vector{String}(undef, 0)
+ id = Vector{String}(undef, 0)
+ if edges
+ lat = Vector{Vector{Float64}}(undef, 0)
+ lon = Vector{Vector{Float64}}(undef, 0)
+
+ for i in eachindex(gdata.delaunayPairs) # iterate through each edge
+ # convert vertex 1 (as a cellNum) to lat, lon and store
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v1)
+ lat_temp1, lon_temp1 = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp1, lat_temp1 = cellToLocation(gdata, gdata.delaunayPairs[i].v1)
+ end
+ # convert vertex 2 (as a cellNum) to lat, lon and store
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v2)
+ lat_temp2, lon_temp2 = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp2, lat_temp2 = cellToLocation(gdata, gdata.delaunayPairs[i].v2)
+ end
+ push!(lat, [lat_temp1, lat_temp2])
+ push!(lon, [lon_temp1, lon_temp2])
+ push!(label, "$(i)")
+ push!(name, "$(i)")
+ push!(id, "$(i)")
+ end
+ else
+ lat = Vector{Union{Missing,Float64}}(undef, 0)
+ lon = Vector{Union{Missing,Float64}}(undef, 0)
+ for i in eachindex(gdata.delaunayPairs) # iterate through each edge
+ # convert vertex 1 (as a cellNum) to lat, lon and store
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v1)
+ lat_temp, lon_temp = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp, lat_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v1)
+ end
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "$(i)")
+ push!(name, "$(i)")
+ push!(id, "$(i)")
+ # convert vertex 2 (as a cellNum) to lat, lon and store
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v2)
+ lat_temp, lon_temp = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp, lat_temp = cellToLocation(gdata, gdata.delaunayPairs[i].v2)
+ end
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "$(i)")
+ push!(name, "$(i)")
+ push!(id, "$(i)")
+ # separate edges with missing values
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(name, "")
+ push!(id, "")
+ end
+ end
+ df_delaunay = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ else
+ @warn("Delaunay network data is missing!")
+ df_delaunay = DataFrames.DataFrame()
+ end
+ return df_source, df_sink, df_network, df_delaunay
+end
+
+function load_solution_delaunay_pairs(gdata::Data, solution::SimccsSolution; edges::Bool=false)
+ label = Vector{String}(undef, 0)
+ name = Vector{String}(undef, 0)
+ id = Vector{String}(undef, 0)
+ if edges
+ lat = Vector{Vector{Float64}}(undef, 0)
+ lon = Vector{Vector{Float64}}(undef, 0)
+ i = 1
+ for edge in eachindex(solution.pipelineFlow) # iterate through each edge
+ if solution.pipelineFlow[edge] > eps(Float16)
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, edge.v1)
+ lat_temp1, lon_temp1 = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp1, lat_temp1 = cellToLocation(gdata, edge.v1)
+ end
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, edge.v2)
+ lat_temp2, lon_temp2 = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp2, lat_temp2 = cellToLocation(gdata, edge.v2)
+ end
+ push!(lat, [lat_temp1, lat_temp2])
+ push!(lon, [lon_temp1, lon_temp2])
+ push!(label, "Flow = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(edge.v1)=>$(edge.v2)")
+ push!(id, "$(i)")
+ i += 1
+ end
+ end
+ else
+ lat = Vector{Union{Missing,Float64}}(undef, 0)
+ lon = Vector{Union{Missing,Float64}}(undef, 0)
+ i = 1
+ for edge in eachindex(solution.pipelineFlow) # iterate through each edge
+ if solution.pipelineFlow[edge] > eps(Float16)
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, edge.v1)
+ lat_temp, lon_temp = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp, lat_temp = cellToLocation(gdata, edge.v1)
+ end
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "Flow = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(edge.v1)=>$(edge.v2)")
+ push!(id, "$(i)")
+ if gdata.projectionVersion == 2
+ x_temp, y_temp = cellToLocation(gdata, edge.v2)
+ lat_temp, lon_temp = AEACPXYtoLatLon([x_temp], [y_temp])
+ else
+ lon_temp, lat_temp = cellToLocation(gdata, edge.v2)
+ end
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "Flow = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(edge.v1)=>$(edge.v2)")
+ push!(id, "$(i)")
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(name, "")
+ push!(id, "")
+ end
+ end
+ end
+ df_solution = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ return df_solution
+end
+
+function load_solution_candidtate_network(gdata::Data, solution::SimccsSolution; edges::Bool=false)
+ label = Vector{String}(undef, 0)
+ name = Vector{String}(undef, 0)
+ id = Vector{String}(undef, 0)
+ if edges
+ lat = Vector{Vector{Float64}}(undef, 0)
+ lon = Vector{Vector{Float64}}(undef, 0)
+ i = 1
+ for edge in eachindex(solution.pipelineFlow) # iterate through each edge
+ if solution.pipelineFlow[edge] > eps(Float16)
+ key = Edge(edge.v1, edge.v2)
+ if haskey(gdata.graphEdgeRoutes, key)
+ route = gdata.graphEdgeRoutes[key]
+ latv = Vector{Float64}(undef, 0)
+ lonv = Vector{Float64}(undef, 0)
+ for cellNum in route
+ lon_temp, lat_temp = cellToLocation(gdata, cellNum)
+ push!(latv, lat_temp)
+ push!(lonv, lon_temp)
+ end
+ if gdata.projectionVersion == 2
+ coordinates = AEACPXYtoLatLon(lonv, latv)
+ push!(lat, coordinates[:,1])
+ push!(lon, coordinates[:,2])
+ else
+ push!(lat, latv)
+ push!(lon, lonv)
+ end
+ push!(label, "Flow = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(key)")
+ push!(id, "$(i)")
+ i += 1
+ else
+ @error("$key does not exist!")
+ throw("Error: data and solution do not match!")
+ end
+ end
+ end
+ else
+ lat = Vector{Union{Missing,Float64}}(undef, 0)
+ lon = Vector{Union{Missing,Float64}}(undef, 0)
+ if gdata.projectionVersion == 2
+ i = 1
+ for edge in eachindex(solution.pipelineFlow) # iterate through each edge
+ if solution.pipelineFlow[edge] > eps(Float16)
+ key = Edge(edge.v1, edge.v2)
+ if haskey(gdata.graphEdgeRoutes, key)
+ route = gdata.graphEdgeRoutes[key]
+ x = Vector{Float64}(undef, 0)
+ y = Vector{Float64}(undef, 0)
+ for cellNum in route
+ x_temp, y_temp = cellToLocation(gdata, cellNum)
+ push!(x, x_temp)
+ push!(y, y_temp)
+ end
+ coordinates = AEACPXYtoLatLon(x, y)
+ for v in coordinates[:,2]
+ push!(lon, v)
+ end
+ for v in coordinates[:,1]
+ push!(lat, v)
+ push!(label, "Flow = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(edge.v1)=>$(edge.v2)")
+ push!(id, "$(i)")
+ end
+ # separate edges with missing values
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(name, "")
+ push!(id, "")
+ i += 1
+ else
+ @error("$key does not exist!")
+ throw("Error: data and solution do not match!")
+ end
+ end
+ end
+ else
+ i = 1
+ for edge in eachindex(solution.pipelineFlow) # iterate through each edge
+ if solution.pipelineFlow[edge] > eps(Float16)
+ key = Edge(edge.v1, edge.v2)
+ if haskey(gdata.graphEdgeRoutes, key)
+ route = gdata.graphEdgeRoutes[key]
+ x = Vector{Float64}(undef, 0)
+ y = Vector{Float64}(undef, 0)
+ for cellNum in route
+ lon_temp, lat_temp = cellToLocation(gdata, cellNum) # convert cellNum to lat, lon
+ push!(lat, lat_temp)
+ push!(lon, lon_temp)
+ push!(label, "Q = $(round(solution.pipelineFlow[edge]; sigdigits=3))
Cost = $(round(solution.pipelineCost[edge]; sigdigits=3))")
+ push!(name, "$(edge.v1)=>$(edge.v2)")
+ push!(id, "$(i)")
+ end
+ # separate edges with missing values
+ push!(lat, missing)
+ push!(lon, missing)
+ push!(label, "")
+ push!(name, "")
+ push!(id, "")
+ i += 1
+ else
+ @error("$key does not exist!")
+ throw("Error: data and solution do not match!")
+ end
+ end
+ end
+ end
+ end
+ df_solution = DataFrames.DataFrame(; id=id, label=label, name=name, lat=lat, lon=lon)
+ return df_solution
+end
+
+function map(gdata::Data; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata; edges=false)
+ map(; df_source=df_source, df_sink=df_sink, df_network=df_network, df_delaunay=df_delaunay, kw...)
+end
+
+function map(gdata::Data, solution::SimccsSolution; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata; edges=false)
+ df_solution = load_solution_candidtate_network(gdata, solution; edges=false)
+ map(; df_source=df_source, df_sink=df_sink, df_network=df_network, df_delaunay=df_delaunay, df_solution=df_solution, kw...)
+end
+
+function map(; df_source::DataFrames.DataFrame=DataFrames.DataFrame(), df_sink::DataFrames.DataFrame=DataFrames.DataFrame(), df_network::DataFrames.DataFrame=DataFrames.DataFrame(), df_delaunay::DataFrames.DataFrame=DataFrames.DataFrame(), df_solution::DataFrames.DataFrame=DataFrames.DataFrame(), filename::AbstractString="", figuredir::AbstractString=".")
+ if size(df_solution, 1) > 1
+ visibility = "legendonly"
+ else
+ visibility = ""
+ end
+ traces = []
+ lon = Vector{Float64}(undef, 0)
+ lat = Vector{Float64}(undef, 0)
+ if size(df_source, 1) > 0
+ sources = PlotlyJS.scattermapbox(;
+ lon = df_source[!, :lon],
+ lat = df_source[!, :lat],
+ name = "Sources",
+ hoverinfo = "text",
+ text = df_source[!, :label],
+ marker = Plotly.attr(; size=10, color="red")
+ )
+ lon = vcat(lon, df_source[!, :lon])
+ lat = vcat(lat, df_source[!, :lat])
+ push!(traces, sources)
+ end
+ if size(df_sink, 1) > 0
+ sinks = PlotlyJS.scattermapbox(;
+ lon = df_sink[!, :lon],
+ lat = df_sink[!, :lat],
+ name = "Sinks",
+ hoverinfo = "text",
+ text = df_sink[!, :label],
+ marker = Plotly.attr(; size=10, color="orange")
+ )
+ lon = vcat(lon, df_sink[!, :lon])
+ lat = vcat(lat, df_sink[!, :lat])
+ push!(traces, sinks)
+ end
+ if size(df_network, 1) > 0
+ network = PlotlyJS.scattermapbox(;
+ lon = df_network[!, :lon],
+ lat = df_network[!, :lat],
+ name = "Candidate Network",
+ mode = "lines",
+ connectgaps = false,
+ hoverinfo = "text",
+ visible = visibility,
+ text = df_network[!, :label],
+ marker = Plotly.attr(; size=10, color="purple")
+ )
+ lon = vcat(lon, collect(skipmissing(df_network[!, :lon])))
+ lat = vcat(lat, collect(skipmissing(df_network[!, :lat])))
+ push!(traces, network)
+ end
+ if size(df_delaunay, 1) > 0
+ delaunay = PlotlyJS.scattermapbox(;
+ lon = df_delaunay[!, :lon],
+ lat = df_delaunay[!, :lat],
+ name = "Delaunay Network",
+ mode = "lines",
+ connectgaps = false,
+ hoverinfo = "text",
+ visible = visibility,
+ text = df_delaunay[!, :label],
+ marker = Plotly.attr(; size=10, color="green")
+ )
+ lon = vcat(lon, collect(skipmissing(df_delaunay[!, :lon])))
+ lat = vcat(lat, collect(skipmissing(df_delaunay[!, :lat])))
+ push!(traces, delaunay)
+ end
+ if size(df_solution, 1) > 0
+ solution = PlotlyJS.scattermapbox(;
+ lon = df_solution[!, :lon],
+ lat = df_solution[!, :lat],
+ name = "Solution",
+ mode = "lines",
+ connectgaps = false,
+ hoverinfo = "text",
+ text = df_solution[!, :label],
+ marker = Plotly.attr(; size=10, color="green")
+ )
+ lon = vcat(lon, collect(skipmissing(df_solution[!, :lon])))
+ lat = vcat(lat, collect(skipmissing(df_solution[!, :lat])))
+ push!(traces, solution)
+ end
+ lonmin = minimum(lon)
+ latmin = minimum(lat)
+ lonmax = maximum(lon)
+ latmax = maximum(lat)
+ lonr = lonmax - lonmin
+ latr = latmax - latmin
+ lonc = lonmin + lonr / 2
+ latc = latmin + latr / 2
+ dx = max(lonr, latr)
+ zoom = dx > 20 ? 3 : 4
+ layout = PlotlyJS.Layout(;
+ plot_bgcolor = "#fff",
+ mapbox = Plotly.attr(; style="stamen-terrain", zoom=zoom, center=Plotly.attr(; lon=lonc, lat=latc)),
+ showlegend = true)
+ p = PlotlyJS.plot(convert(Array{typeof(traces[1])}, traces), layout)
+ if filename != ""
+ fn = joinpathcheck(figuredir, filename)
+ recursivemkdir(fn)
+ PlotlyJS.savefig(p, fn; format="html")
+ end
+ return p
+end
+
+function map_sources(gdata::Data; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata)
+ map_data(df_source; name="Sources", mode="markers", kw...)
+end
+
+function map_sinks(gdata::Data; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata)
+ map_data(df_sink; name="Sinks", mode="markers", kw...)
+end
+
+function map_candidate_network(gdata::Data; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata; edges=true)
+ map_data(df_network; name="Candidate Network", edges=true, kw...)
+end
+
+function map_delaunay_network(gdata::Data; kw...)
+ df_source, df_sink, df_network, df_delaunay = load_dataframes(gdata; edges=true)
+ map_data(df_delaunay; name="Delaunay", edges=true, kw...)
+end
+
+function map_solution(gdata::Data, solution::SimccsSolution; kw...)
+ df_solution = load_solution_candidtate_network(gdata, solution; edges=true)
+ map_data(df_solution; name="SimccsSolution", edges=true, kw...)
+end
+
+function map_data(df::DataFrames.DataFrame; port::Integer=8050, ip=string(Sockets.getipaddr()), name::AbstractString="", color::AbstractString="#0074D9", color_selected::AbstractString="#7FDBFF", mode::AbstractString="markers", edges::Bool=false)
+ if edges
+ lon = vcat(df[!, :lon]...)
+ lat = vcat(df[!, :lat]...)
+ else
+ lon = collect(skipmissing(df[!, :lon]))
+ lat = collect(skipmissing(df[!, :lat]))
+ end
+ lonmin = minimum(lon)
+ latmin = minimum(lat)
+ lonmax = maximum(lon)
+ latmax = maximum(lat)
+ lonr = lonmax - lonmin
+ latr = latmax - latmin
+ lonc = lonmin + lonr / 2
+ latc = latmin + latr / 2
+ dx = max(lonr, latr)
+ zoom = dx > 20 ? 3 : 4
+ layout = PlotlyJS.Layout(;
+ plot_bgcolor = "#fff",
+ height = 800,
+ mapbox = Plotly.attr(; style="stamen-terrain", zoom=zoom, center=Plotly.attr(; lon=lonc, lat=latc)),
+ showlegend = true)
+ app = Dash.dash(; suppress_callback_exceptions=true)
+ app.layout = Dash.html_div([
+ Dash.dash_datatable(
+ id="interactive-table",
+ columns=[Dict("name"=>i, "id"=>i, "deletable"=>true, "selectable"=>true) for i in names(df)[1:end-2]],
+ data=Dict.(pairs.(eachrow(df))),
+ editable=true,
+ filter_action="native",
+ sort_action="native",
+ sort_mode="multi",
+ column_selectable="single",
+ row_selectable="multi",
+ row_deletable=true,
+ selected_rows=[],
+ selected_columns=[],
+ page_action="native",
+ page_current=0,
+ page_size=10
+ ),
+ Dash.html_div(id="interactive-map")
+ ])
+ Dash.callback!(app,
+ Dash.Output("interactive-table", "style_data_conditional"),
+ Dash.Input("interactive-table", "selected_columns")
+ ) do selected_columns
+ return [Dict("if"=>Dict("column_id"=>i), "background_color"=>"#CCC") for i in selected_columns]
+ end
+ Dash.callback!(app,
+ Dash.Output("interactive-map", "children"),
+ Dash.Input("interactive-table", "derived_virtual_data"),
+ Dash.Input("interactive-table", "derived_virtual_selected_rows")
+ ) do derived_virtual_data, derived_virtual_selected_rows
+ dff = (derived_virtual_data isa Nothing) ? df : DataFrames.DataFrame(derived_virtual_data)
+ global dash_df = dff
+ if derived_virtual_selected_rows isa Nothing
+ derived_virtual_selected_rows = []
+ else
+ derived_virtual_selected_rows = derived_virtual_selected_rows .+ 1
+ end
+ if edges
+ traces = []
+ for i in 1:DataFrames.nrow(dff)
+ r = dff[i, :]
+ colorl = i in derived_virtual_selected_rows ? color_selected : color
+ push!(traces, PlotlyJS.scattermapbox(;
+ lon = r.lon,
+ lat = r.lat,
+ name = name * " line $i",
+ mode = "lines",
+ hoverinfo = "none",
+ showlegend = false,
+ line = Plotly.attr(color=colorl; width=2)
+ ))
+ end
+ for i in 1:DataFrames.nrow(dff)
+ colorp = i in derived_virtual_selected_rows ? color_selected : color
+ r = dff[i, :]
+ if length(r.lon) > 2
+ lon = [r.lon[convert(Int64, floor(length(r.lon) / 2))]]
+ lat = [r.lat[convert(Int64, floor(length(r.lat) / 2))]]
+ else
+ lon = [sum(r.lon) / length(r.lon)]
+ lat = [sum(r.lat) / length(r.lat)]
+ end
+ push!(traces, PlotlyJS.scattermapbox(;
+ lon = lon,
+ lat = lat,
+ name = name * " point $i",
+ mode = "markers",
+ hoverinfo = "text",
+ text = [r.id],
+ showlegend = false,
+ marker = Plotly.attr(; size=9, color=colorp)
+ ))
+ end
+ p = convert(Array{typeof(traces[1])}, traces)
+ else
+ colors = [(i in derived_virtual_selected_rows ? color_selected : color) for i in 1:DataFrames.nrow(dff)]
+ p = PlotlyJS.scattermapbox(;
+ lon = dff[!, :lon],
+ lat = dff[!, :lat],
+ name = name,
+ mode = mode,
+ connectgaps = false,
+ hoverinfo = "text",
+ text = dff[!, :id],
+ line = Plotly.attr(color=colors),
+ marker = Plotly.attr(; size=10, color=colors)
+ )
+ end
+ return [Dash.dcc_graph(id="interactive-plot", figure=PlotlyJS.plot(p, layout))]
+ end
+ Dash.callback!(app,
+ Dash.Output("interactive-table", "selected_rows"),
+ Dash.Input("interactive-plot", "selectedData"),
+ Dash.Input("interactive-plot", "clickData")
+ ) do selectedData, clickData
+ if !(selectedData isa Nothing)
+ if edges
+ # @show selectedData
+ ids = [parse(Int64, i.text) for i in selectedData.points]
+ lbs = parse.(Int64, dash_df[!, :id])
+ s = indexin(ids, lbs) .- 1
+ else
+ s = [i.pointIndex for i in selectedData.points]
+ end
+ return s
+ elseif !(clickData isa Nothing)
+ if edges
+ # @show clickData
+ ids = Vector{Int64}(undef, 0)
+ for i in clickData.points
+ if haskey(i, :text)
+ push!(ids, parse(Int64, i.text))
+ end
+ end
+ lbs = parse.(Int64, dash_df[!, :id])
+ s = indexin(ids, lbs) .- 1
+ else
+ s = [i.pointIndex for i in clickData.points]
+ end
+ return s
+ else
+ Dash.throw(Dash.PreventUpdate())
+ end
+ end
+ stop_dash_server()
+ start_dash_server(app; ip=ip, port=port)
+end
+
+function start_dash_server(app; port::Integer=8050, ip=string(Sockets.getipaddr()))
+ Dash.enable_dev_tools!(app; debug=true, dev_tools_ui=true, dev_tools_serve_dev_bundles=true)
+ handler = Dash.make_handler(app)
+ global dash_server = Sockets.listen(Dash.get_inetaddr(ip, port))
+ global dash_task = @async HTTP.serve(handler, ip, port; server=dash_server, verbose=true)
+end
+
+function stop_dash_server()
+ if isdefined(SimCCSpro, :dash_server)
+ close(dash_server)
+ end
+end
\ No newline at end of file
diff --git a/scripts/example_dash.jl b/scripts/dash_table_example.jl
similarity index 100%
rename from scripts/example_dash.jl
rename to scripts/dash_table_example.jl