Skip to content

Commit 917e7ea

Browse files
committed
prepare for multiple ts plots
1 parent 11642f5 commit 917e7ea

File tree

4 files changed

+172
-42
lines changed

4 files changed

+172
-42
lines changed

NetworkDynamicsInspector/assets/app.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ html, body {
5555
overflow: hidden;
5656
/* grid-column: 2; */
5757
}
58+
.timeseries-card.active-tseries {
59+
box-shadow: 0 4px 8px rgba(0.0, 0.0, 51.0, 0.65);
60+
}
5861
.timeseries-card-container{
5962
display: flex;
6063
flex-direction: column;

NetworkDynamicsInspector/src/NetworkDynamicsInspector.jl

Lines changed: 144 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ using NetworkDynamics: SII
1212
using Graphs: nv, ne
1313
using GraphMakie
1414
using GraphMakie.NetworkLayout
15+
using Bonito.Hyperscript
16+
using Bonito.Tables.OrderedCollections
1517

1618
const JQUERY = Asset("https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js")
1719
const SELECT2_CSS = Asset("https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css")
@@ -36,7 +38,7 @@ function apptheme()
3638
)
3739
end
3840

39-
function graphplot_card(app; kwargs...)
41+
function graphplot_card(app, session)
4042
nw = map!(extract_nw, Observable{Network}(), app.sol)
4143
NV = nv(nw[])
4244
NE = ne(nw[])
@@ -110,11 +112,11 @@ function graphplot_card(app; kwargs...)
110112
SMALL = 30
111113
BIG = 50
112114
node_size = Observable(fill(SMALL, NV))
113-
THIN = 3
114-
THICK = 6
115+
THIN = 5
116+
THICK = 8
115117
edge_width = Observable(fill(THIN, NE))
116118

117-
onany(app.graphplot.selcomp; update=true) do selcomp
119+
onany(app.graphplot._selcomp; update=true) do selcomp
118120
@debug "GP: Sel comp => node_size, edge_width"
119121
fill!(node_size[], SMALL)
120122
fill!(edge_width[], THIN)
@@ -144,6 +146,7 @@ function graphplot_card(app; kwargs...)
144146
Stress(; pin)
145147
end
146148

149+
small_hover_text = Observable{String}("")
147150
fig, ax = with_theme(apptheme()) do
148151
fig = Figure(; figure_padding=0)
149152
ax = Axis(fig[1,1])
@@ -153,6 +156,11 @@ function graphplot_card(app; kwargs...)
153156

154157
hidespines!(ax)
155158
hidedecorations!(ax)
159+
160+
fig[1,1] = Label(fig, small_hover_text,
161+
tellwidth=false, tellheight=false,
162+
justification=:left, halign=:left, valign=:bottom)
163+
156164
fig, ax
157165
end
158166
xratio = Ref{Float64}(1.0)
@@ -162,7 +170,59 @@ function graphplot_card(app; kwargs...)
162170
adapt_xy_scaling!(xratio, yratio, ax)
163171
nothing
164172
end
165-
Card(fig; class="graphplot-card", kwargs...)
173+
174+
####
175+
#### Interactions
176+
####
177+
hoverstate = Observable(false)
178+
179+
js = js"""
180+
const gpcard = document.querySelector(".graphplot-card");
181+
$(hoverstate).on((state) => {
182+
if (state) {
183+
gpcard.style.cursor = "pointer";
184+
} else {
185+
gpcard.style.cursor = "default";
186+
}
187+
});
188+
"""
189+
evaljs(session, js)
190+
191+
nhh = NodeHoverHandler() do hstate, idx, event, axis
192+
hoverstate[] = hstate
193+
app.graphplot._hoverel[] = hstate ? VIndex(idx) : nothing
194+
end
195+
ehh = EdgeHoverHandler() do hstate, idx, event, axis
196+
hoverstate[] = hstate
197+
app.graphplot._hoverel[] = hstate ? EIndex(idx) : nothing
198+
end
199+
200+
on(app.graphplot._hoverel) do el
201+
small_hover_text[] = isnothing(el) ? "" : repr(el)
202+
end
203+
204+
# interactions
205+
clickaction = (i, type) -> begin
206+
idx = type == :vertex ? VIndex(i) : EIndex(i)
207+
selcomp = app.tsplots[][app.active_tsplot[]].selcomp
208+
if idx selcomp[]
209+
push!(selcomp[], idx)
210+
else
211+
filter!(x -> x != idx, selcomp[])
212+
end
213+
app.graphplot._lastclickel[] = idx
214+
notify(selcomp)
215+
end
216+
nch = NodeClickHandler((i, _, _) -> clickaction(i, :vertex))
217+
ech = EdgeClickHandler((i, _, _) -> clickaction(i, :edge))
218+
219+
register_interaction!(ax, :nodeclick, nch)
220+
register_interaction!(ax, :nodehover, nhh)
221+
register_interaction!(ax, :edgeclick, ech)
222+
register_interaction!(ax, :edgehover, ehh)
223+
224+
225+
Card(fig; class="graphplot-card")
166226
end
167227
function _gracefully_extract_states!(vec, sol, t, idxs, rel)
168228
isvalid(s) = SII.is_variable(sol, s) || SII.is_parameter(sol, s) || SII.is_observed(sol, s)
@@ -358,7 +418,42 @@ function _maxrange(sol, idxs, rel)
358418
extrema(Iterators.flatten(u_for_t))
359419
end
360420

361-
function timeseries_card(app, session)
421+
function timeseries_cards(app, session)
422+
cards = OrderedDict{String,Hyperscript.Node{Hyperscript.HTMLSVG}}()
423+
container = Observable{Hyperscript.Node{Hyperscript.HTMLSVG}}()
424+
425+
on(app.tsplots; update=true) do _tsplots
426+
@debug "TS: app.tsplots => update timeseries cards"
427+
newkeys = keys(_tsplots)
428+
knownkeys = keys(cards)
429+
430+
for delkey in setdiff(knownkeys, newkeys)
431+
delete!(cards, delkey)
432+
end
433+
for newkey in setdiff(newkeys, knownkeys)
434+
cards[newkey] = timeseries_card(app, newkey, session)
435+
# cards[newkey] = DOM.div(scatter(rand(100)))
436+
end
437+
if keys(cards) != keys(_tsplots)
438+
@warn "The keys do not match: $(keys(cards)) vs $(keys(_tsplots))"
439+
end
440+
441+
container[] = DOM.div(values(cards)...; class="timeseries-stack")
442+
443+
nothing
444+
end
445+
446+
on(app.active_tsplot; update=true) do active
447+
activesel = app.tsplots[][active].selcomp[]
448+
app.graphplot._selcomp[] = activesel
449+
end
450+
451+
return container[]
452+
end
453+
454+
function timeseries_card(app, key, session)
455+
tsplot = app.tsplots[][key]
456+
362457
comp_options = Observable{Vector{OptionGroup{SymbolicIndex}}}()
363458
on(app.sol; update=true) do _sol
364459
@debug "TS: app.sol => comp_options"
@@ -370,21 +465,21 @@ function timeseries_card(app, session)
370465
end
371466

372467
state_options = Observable{Vector{OptionGroup{Symbol}}}()
373-
onany(app.sol, app.tsplot.selcomp; update=true) do _sol, _sel
374-
@debug "TS: app.sol, app.tsplot.selcomp => state_options"
468+
onany(app.sol, tsplot.selcomp; update=true) do _sol, _sel
469+
@debug "TS: app.sol, tsplot.selcomp => state_options"
375470
_nw = extract_nw(_sol)
376471
state_options[] = gen_state_options(_nw, _sel)
377472
nothing
378473
end
379474

380-
comp_sel = MultiSelect(comp_options, app.tsplot.selcomp;
475+
comp_sel = MultiSelect(comp_options, tsplot.selcomp;
381476
placeholder="Select components",
382477
multi=true,
383478
option_to_string=_sidx_to_str,
384479
T=SymbolicIndex,
385480
id=gendomid("compsel"))
386481
# comp_sel_dom = Grid(DOM.span("Components"), comp_sel; columns = "70px 1fr", align_items = "center")
387-
state_sel = MultiSelect(state_options, app.tsplot.states;
482+
state_sel = MultiSelect(state_options, tsplot.states;
388483
placeholder="Select states",
389484
multi=true,
390485
T=Symbol,
@@ -394,11 +489,11 @@ function timeseries_card(app, session)
394489
on(reset_button.value) do _
395490
empty!(color_cache)
396491
empty!(linestyle_cache)
397-
notify(app.tsplot.selcomp)
398-
notify(app.tsplot.states)
492+
notify(tsplot.selcomp)
493+
notify(tsplot.states)
399494
end
400495

401-
rel_toggle = ToggleSwitch(value=app.tsplot.rel, label="Rel to u0")
496+
rel_toggle = ToggleSwitch(value=tsplot.rel, label="Rel to u0")
402497

403498
comp_state_sel_dom = Grid(
404499
DOM.span("Components"), comp_sel, reset_button,
@@ -409,11 +504,9 @@ function timeseries_card(app, session)
409504
)
410505

411506
# hl choice of elements in graphplot
412-
on(app.tsplot.selcomp; update=true) do _sel
507+
on(tsplot.selcomp; update=true) do _sel
413508
@debug "TS: comp selection => graphplot selection"
414-
if app.graphplot.selcomp[] != _sel
415-
app.graphplot.selcomp[] = _sel
416-
end
509+
app.graphplot._selcomp[] = _sel
417510
nothing
418511
end
419512

@@ -429,7 +522,7 @@ function timeseries_card(app, session)
429522
colorpairs = Observable{Vector{@NamedTuple{title::String,color::String}}}()
430523
lstylepairs = Observable{Vector{@NamedTuple{title::String,linestyle::String}}}()
431524

432-
on(app.tsplot.selcomp; update=true) do _sel
525+
on(tsplot.selcomp; update=true) do _sel
433526
@debug "TS: comp selection => update color_cache"
434527
for unused in setdiff(keys(color_cache), _sel)
435528
delete!(color_cache, unused)
@@ -443,7 +536,7 @@ function timeseries_card(app, session)
443536
for (k,v) in color_cache]
444537
nothing
445538
end
446-
on(app.tsplot.states; update=true) do _states
539+
on(tsplot.states; update=true) do _states
447540
@debug "TS: state selection => update linestyle_cache"
448541
for unused in setdiff(keys(linestyle_cache), _states)
449542
delete!(linestyle_cache, unused)
@@ -567,16 +660,16 @@ function timeseries_card(app, session)
567660
# lastupdate[] = Inf
568661
# end
569662
# end
570-
on(session.on_close) do _
571-
@info "Session closed, time to clean up"
572-
# close(timer)
573-
end
663+
# on(session.on_close) do _
664+
# @info "Session closed, time to clean up"
665+
# # close(timer)
666+
# end
574667

575668
# collect all the states wie might want to plot
576669
valid_idxs = Observable(
577670
Union{VIndex{Int,Symbol},EIndex{Int,Symbol}}[]
578671
)
579-
onany(app.tsplot.selcomp, app.tsplot.states; update=true) do _selcomp, _states
672+
onany(tsplot.selcomp, tsplot.states; update=true) do _selcomp, _states
580673
@debug "TS: sel comp/states => update valid_idxs"
581674
isvalid(s) = SII.is_variable(app.sol[], s) || SII.is_parameter(app.sol[], s) || SII.is_observed(app.sol[], s)
582675
empty!(valid_idxs[])
@@ -589,7 +682,7 @@ function timeseries_card(app, session)
589682

590683
# extract the data
591684
data = Observable{Vector{Vector{Float32}}}(Vector{Float32}[])
592-
onany(ts, valid_idxs, app.tsplot.rel, app.sol; update=true) do _ts, _valid_idxs, _rel, _sol
685+
onany(ts, valid_idxs, tsplot.rel, app.sol; update=true) do _ts, _valid_idxs, _rel, _sol
593686
@debug "TS: t, valid_idx, rel, sol => update data"
594687
_dat = _sol(_ts, idxs=_valid_idxs)
595688
if _rel
@@ -640,14 +733,38 @@ function timeseries_card(app, session)
640733
end
641734
register_interaction!(set_time_interaction, ax, :set_time)
642735

643-
Card(
736+
cardclass = "timeseries-card"
737+
if key == app.active_tsplot[]
738+
cardclass *= " active-tseries"
739+
end
740+
741+
card = Card(
644742
DOM.div(
645743
comp_state_sel_dom,
646744
DOM.div(fig; class="timeseries-axis-container");
647745
class="timeseries-card-container"
648746
);
649-
class="timeseries-card"
747+
class=cardclass
650748
)
749+
750+
# on click set active-tseries class
751+
click = js"""
752+
(card) => {
753+
card.addEventListener("click", function(event) {
754+
$(app.active_tsplot).notify($(key));
755+
756+
document.querySelectorAll(".timeseries-card").forEach(element => {
757+
element.classList.remove("active-tseries");
758+
});
759+
760+
// Add "active-tseries" to the given target element
761+
card.classList.add("active-tseries");
762+
}, { capture: true });
763+
}
764+
"""
765+
Bonito.onload(session, card, click)
766+
767+
return card
651768
end
652769
function _sidx_to_str(s)
653770
(s isa VIndex ? "v" : "e") * string(s.compidx)

NetworkDynamicsInspector/src/utils.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function NetworkDynamics.extract_nw(o::NamedTuple)
6868
end
6969

7070
getcycled(v::AbstractVector, i) = v[mod1(i, length(v))]
71-
gendomid(s::String) = replace(string(gensym("selectbox")), "#"=>"")
71+
gendomid(s::String) = replace(string(gensym(s)), "#"=>"")
7272

7373
mutable struct Throttle{F}
7474
f::F

test/Inspector_test.jl

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ using GraphMakie
88
using Graphs: SimpleGraph
99
using OrdinaryDiffEqTsit5
1010
using Graphs: Graphs
11+
using OrderedCollections
1112
include(joinpath(pkgdir(NetworkDynamics), "test", "ComponentLibrary.jl"))
1213

1314
sol = let
@@ -62,9 +63,8 @@ app = (;
6263
t = Observable{Float64}(0.0),
6364
tmin = Observable{Float64}(sol.t[begin]),
6465
tmax = Observable{Float64}(sol.t[end]),
65-
active_tsplot = Observable{Int}(1),
66+
active_tsplot = Observable{String}("a"),
6667
graphplot = (;
67-
selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
6868
nstate = Observable{Vector{Symbol}}([]),
6969
estate = Observable{Vector{Symbol}}([:P]),
7070
nstate_rel = Observable{Bool}(false),
@@ -73,12 +73,27 @@ app = (;
7373
ncolorscheme = Observable{ColorScheme}(ColorSchemes.coolwarm),
7474
ecolorrange = Observable{Tuple{Float32,Float32}}((-1.0, 1.0)),
7575
ecolorscheme = Observable{ColorScheme}(ColorSchemes.coolwarm),
76+
_selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
77+
_hoverel = Observable{Union{EIndex{Int,Nothing},VIndex{Int,Nothing},Nothing}}(nothing)
78+
_lastclickel = Observable{Union{EIndex{Int,Nothing},VIndex{Int,Nothing},Nothing}}(nothing)
7679
),
77-
tsplot = (;
78-
selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
79-
states = Observable{Vector{Symbol}}(Symbol[]),
80-
rel = Observable{Bool}(false),
81-
)
80+
# tsplot = (;
81+
# selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
82+
# states = Observable{Vector{Symbol}}(Symbol[]),
83+
# rel = Observable{Bool}(false),
84+
# ),
85+
tsplots = Observable{Any}(OrderedDict(
86+
"a" => (;
87+
selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
88+
states = Observable{Vector{Symbol}}(Symbol[]),
89+
rel = Observable{Bool}(false),
90+
),
91+
"b" => (;
92+
selcomp = Observable{Vector{SymbolicIndex}}(SymbolicIndex[]),
93+
states = Observable{Vector{Symbol}}(Symbol[]),
94+
rel = Observable{Bool}(false),
95+
),
96+
))
8297
);
8398

8499
let
@@ -129,19 +144,14 @@ let
129144
DOM.div(
130145
NetworkDynamicsInspector.APP_CSS,
131146
DOM.div(
132-
NetworkDynamicsInspector.graphplot_card(app),
147+
NetworkDynamicsInspector.graphplot_card(app, session),
133148
NetworkDynamicsInspector.gpstate_control_card(app, :vertex),
134149
NetworkDynamicsInspector.gpstate_control_card(app, :edge),
135150
class="graphplot-col"
136151
),
137152
DOM.div(
138153
NetworkDynamicsInspector.timeslider_card(app),
139-
DOM.div(
140-
NetworkDynamicsInspector.timeseries_card(app, session),
141-
Card("foo"),
142-
Card("bar"),
143-
class="timeseries-stack"
144-
),
154+
NetworkDynamicsInspector.timeseries_cards(app, session),
145155
class="timeseries-col"
146156
),
147157
class="maingrid"

0 commit comments

Comments
 (0)