From 4c92da78feb4307e43e28a018fcad7e3567d7a88 Mon Sep 17 00:00:00 2001 From: Vladimir Mikheev Date: Wed, 8 Jan 2025 14:38:04 +0100 Subject: [PATCH] pruning and more docstrings --- old/unused_functions.jl | 150 ++++++++++ src/ERPExplorer.jl | 102 +------ src/explore.jl | 93 ++++++ src/{functions.jl => functions_formular.jl} | 82 +----- src/{plot_data.jl => functions_plotting.jl} | 80 +++++- ...xtractor.jl => functions_preprocessing.jl} | 0 src/widgets.jl | 270 ------------------ src/widgets_long.jl | 123 ++++++++ src/widgets_short.jl | 50 ++++ 9 files changed, 496 insertions(+), 454 deletions(-) create mode 100644 old/unused_functions.jl create mode 100644 src/explore.jl rename src/{functions.jl => functions_formular.jl} (57%) rename src/{plot_data.jl => functions_plotting.jl} (64%) rename src/{formula_extractor.jl => functions_preprocessing.jl} (100%) delete mode 100644 src/widgets.jl create mode 100644 src/widgets_long.jl create mode 100644 src/widgets_short.jl diff --git a/old/unused_functions.jl b/old/unused_functions.jl new file mode 100644 index 0000000..d00ce61 --- /dev/null +++ b/old/unused_functions.jl @@ -0,0 +1,150 @@ +function rectselect(ax) + selrect, h = select_vspan(ax.scene; color = (0.9)) + translate!(h, 0, 0, -1) # move to background + return selrect +end + +function Bonito.jsrender(s::Session, selector::SelectSet) + rows = map(selector.items[]) do value + c = Bonito.Checkbox(true; class = "p-1 m-1") + on(s, c.value) do x + values = selector.value[] + has_item = (value in values) + if x + !has_item && push!(values, value) + else + has_item && filter!(x -> x != value, values) + end + notify(selector.value) + end + return Row(value, c) + end + return Bonito.jsrender(s, Card(Col(rows...))) +end + +function style_map(::AbstractRange{<:Number}) + return Dict( + :color => identity, + :colormap => RGBAf.(Colors.color.(to_colormap(:lighttest)), 0.5), + ) +end + +function style_map(values::Set) + mpalette = [:circle, :star4, :xcross, :diamond] + dict = Dict(v => mpalette[i] for (i, v) in enumerate(values)) + mcmap = Makie.wong_colors(0.5) + mcolor_lookup = Dict(v => mcmap[i] for (i, v) in enumerate(values)) + return Dict(:marker => v -> dict[v], :marker_color => mcolor_lookup) +end + +function select_vspan(scene; blocking = false, priority = 2, kwargs...) + key = Mouse.left + waspressed = Observable(false) + rect = Observable(Rectf(0, 0, 1, 1)) # plotted rectangle + rect_ret = Observable(Rectf(0, 0, 1, 1)) # returned rectangle + + # Create an initially hidden rectangle + low = Observable(0.0f0) + high = Observable(0.0f0) + + on(rect) do r + low.val = r.origin[1] + high[] = r.origin[1] + r.widths[1] + end + plotted_span = vspan!( + scene, + low, + high, + visible = false, + kwargs..., + transparency = true, + color = (:black, 0.1), + ) + + on(events(scene).mousebutton, priority = priority) do event + if event.button == key + if event.action == Mouse.press && is_mouseinside(scene) + mp = mouseposition(scene) + waspressed[] = true + plotted_span[:visible] = true # start displaying + rect[] = Rectf(mp, 0.0, 0.0) + return Consume(blocking) + end + end + if !(event.button == key && event.action == Mouse.press) + if waspressed[] # User has selected the rectangle + waspressed[] = false + r = Makie.absrect(rect[]) + w, h = widths(r) + rect_ret[] = r + end + # always hide if not the right key is pressed + #plotted_span[:visible] = false # make the plotted rectangle invisible + return Consume(blocking) + end + + return Consume(false) + end + on(events(scene).mouseposition, priority = priority) do event + if waspressed[] + mp = mouseposition(scene) + mini = minimum(rect[]) + rect[] = Rectf(mini, mp - mini) + return Consume(blocking) + end + return Consume(false) + end + + return rect_ret, plotted_span +end + +function Bonito.jsrender(s::Session, selector::SelectSet) + rows = map(selector.items[]) do value + c = Bonito.Checkbox(true; class = "p-1 m-1") + on(s, c.value) do x + values = selector.value[] + has_item = (value in values) + if x + !has_item && push!(values, value) + else + has_item && filter!(x -> x != value, values) + end + notify(selector.value) + end + return Row(value, c) + end + return Bonito.jsrender(s, Card(Col(rows...))) +end + +function variable_legend(name, values::AbstractRange{<:Number}, palette::Dict) + range, cmap = palette[:colormap] + return S.Colorbar(limits = range, colormap = cmap, label = string(name)) +end + +function variable_legend(name, values::Set, palette::Dict) + marker_color_lookup = (x) -> begin + if haskey(palette, :color) + return get(palette[:color], x, :black) + else + return :black + end + end + marker_lookup = (x) -> begin + if haskey(palette, :marker) + return palette[:marker][x] + else + return :rect + end + end + conditions = collect(values) + elements = map(conditions) do c + return MarkerElement(marker = marker_lookup(c), color = marker_color_lookup(c)) + end + return S.Legend(elements, conditions) +end + + +widget_value(w::Vector{<:String}; resolution = 1) = w +widget_value(x::Vector; resolution = 1) = + x[1] ≈ x[end] ? Float64[] : range(Float64(x[1]), Float64(x[end]), length = 5) + diff --git a/src/ERPExplorer.jl b/src/ERPExplorer.jl index d6ee432..804ce5a 100644 --- a/src/ERPExplorer.jl +++ b/src/ERPExplorer.jl @@ -17,103 +17,13 @@ using StatsModels using StatsBase using TopoPlots -include("formula_extractor.jl") -include("functions.jl") -include("widgets.jl") -include("plot_data.jl") +include("explore.jl") +include("functions_preprocessing.jl") +include("functions_formular.jl") +include("functions_plotting.jl") -""" - explore(model::UnfoldModel; positions = nothing, size = (700, 600)) -Run the dashboard for explorative ERP analysis. +include("widgets_short.jl") +include("widgets_long.jl") -Arguments:\\ -- `model::UnfoldLinearModel{Float64}` - Unfold linear model with categorical and continuous terms. -- `positions::Vector{Point{2, Float32}}` - x an y coordinates of the channels. -- `size::Tuple{Float64, Float64}` - size of the topoplot panel. - -Actions: -- Extract formula terms and itheir features. -- Create interactive formula with checkboxes. -- Arrange and map dropdown menus. -- Create interactive topoplot. -- Create Observable DataFrame with predicted values (yhats) and more. -- Create `GridLayout`. -- Use `Base.ReentrantLock`, a synchronization primitive_ to manage concurrent access to shared resources in multi-threaded programs - - -**Return Value:** `res::Hyperscript.Node{Hyperscript.HTMLSVG}` - final HTML code of the dashboard. -""" -function explore(model::UnfoldModel; positions = nothing, size = (700, 600)) - App() do - variables = extract_variables(model) - widget_checkbox, widget_signal, widget_dom, value_ranges = - formular_widgets(variables) - - var_types = map(x -> x[2][3], variables) - varnames = first.(variables) - - mapping, mapping_dom = mapping_dropdowns(varnames, var_types) - - if isnothing(positions) - channel = Observable(1) - topo_widget = nothing - else - topo_widget, channel = topoplot_widget(positions; size = size .* 0.5) - end - - yhats_sig = yhats_signal(model, widget_signal, channel) - on(mapping) do m - ws = widget_signal.val - ks_m = values(m) - ks_ws = [w.first for w in ws] - for k in ks_ws - widget_checkbox[k][] = k ∈ ks_m - end - end - - obs = Observable(S.GridLayout()) - l = Base.ReentrantLock() - - Makie.onany_latest(yhats_sig, mapping; update = true) do eff, mapping # update = true means only, that it is run once immediately - lock(l) do - obs[] = plot_data( - eff, - value_ranges, - varnames[var_types.==:CategoricalTerm], - varnames[var_types.==:ContinuousTerm], - mapping, - ) - end - return - end - css = Asset(joinpath(@__DIR__, "..", "style.css")) - fig = plot(obs; figure = (size = size,)) - res = DOM.div( - css, - Bonito.TailwindCSS, - Grid( - Card(widget_dom, style = Styles("grid-area" => "header")), - Card(mapping_dom, style = Styles("grid-area" => "sidebar")), - Card(topo_widget, style = Styles("grid-area" => "topo")), - Card(fig, style = Styles("grid-area" => "content")); - columns = "5fr 1fr", - rows = "1fr 6fr 4fr", - areas = """ -'header header' -'content sidebar' -'content topo' -""", - ); - style = Styles( - "height" => "$(1.2*size[2])px", - "width" => "$(size[1])px", - "margin" => "20px", - "position" => :relative, - ), - ) - return res - end -end -export explore end diff --git a/src/explore.jl b/src/explore.jl new file mode 100644 index 0000000..ac4a223 --- /dev/null +++ b/src/explore.jl @@ -0,0 +1,93 @@ +""" + explore(model::UnfoldModel; positions = nothing, size = (700, 600)) +Run the dashboard for explorative ERP analysis. + +Arguments:\\ +- `model::UnfoldLinearModel{Float64}` - Unfold linear model with categorical and continuous terms. +- `positions::Vector{Point{2, Float32}}` - x an y coordinates of the channels. +- `size::Tuple{Float64, Float64}` - size of the topoplot panel. + +Actions: +- Extract formula terms and itheir features. +- Create interactive formula with checkboxes. +- Arrange and map dropdown menus. +- Create interactive topoplot. +- Create Observable DataFrame with predicted values (yhats) and more. +- Create `GridLayout`. +- Use `Base.ReentrantLock`, a synchronization primitive_ to manage concurrent access to shared resources in multi-threaded programs +- Create Figure. +- Translate into into HTML code using DOMs. + +**Return Value:** `Hyperscript.Node{Hyperscript.HTMLSVG}` - final HTML code of the dashboard. +""" +function explore(model::UnfoldModel; positions = nothing, size = (700, 600)) + App() do + variables = extract_variables(model) + widget_checkbox, widget_signal, widget_dom, value_ranges = + formular_widgets(variables) + + var_types = map(x -> x[2][3], variables) + varnames = first.(variables) + + mapping, mapping_dom = mapping_dropdowns(varnames, var_types) + + if isnothing(positions) + channel = Observable(1) + topo_widget = nothing + else + topo_widget, channel = topoplot_widget(positions; size = size .* 0.5) + end + + yhats_sig = yhats_signal(model, widget_signal, channel) + on(mapping) do m + ws = widget_signal.val + ks_m = values(m) + ks_ws = [w.first for w in ws] + for k in ks_ws + widget_checkbox[k][] = k ∈ ks_m + end + end + + obs = Observable(S.GridLayout()) + l = Base.ReentrantLock() + + Makie.onany_latest(yhats_sig, mapping; update = true) do eff, mapping # update = true means only, that it is run once immediately + lock(l) do + obs[] = plot_data( + eff, + value_ranges, + varnames[var_types.==:CategoricalTerm], + varnames[var_types.==:ContinuousTerm], + mapping, + ) + end + return + end + css = Asset(joinpath(@__DIR__, "..", "style.css")) + fig = plot(obs; figure = (size = size,)) + res = DOM.div( + css, + Bonito.TailwindCSS, + Grid( + Card(widget_dom, style = Styles("grid-area" => "header")), + Card(mapping_dom, style = Styles("grid-area" => "sidebar")), + Card(topo_widget, style = Styles("grid-area" => "topo")), + Card(fig, style = Styles("grid-area" => "content")); + columns = "5fr 1fr", + rows = "1fr 6fr 4fr", + areas = """ +'header header' +'content sidebar' +'content topo' +""", + ); + style = Styles( + "height" => "$(1.2*size[2])px", + "width" => "$(size[1])px", + "margin" => "20px", + "position" => :relative, + ), + ) + return res + end +end diff --git a/src/functions.jl b/src/functions_formular.jl similarity index 57% rename from src/functions.jl rename to src/functions_formular.jl index b70be2b..590713b 100644 --- a/src/functions.jl +++ b/src/functions_formular.jl @@ -1,38 +1,7 @@ -function variable_legend(name, values::AbstractRange{<:Number}, palette::Dict) - range, cmap = palette[:colormap] - return S.Colorbar(limits = range, colormap = cmap, label = string(name)) -end - -function variable_legend(name, values::Set, palette::Dict) - - marker_color_lookup = (x) -> begin - if haskey(palette, :color) - return get(palette[:color], x, :black) - else - return :black - end - end - marker_lookup = (x) -> begin - if haskey(palette, :marker) - return palette[:marker][x] - else - return :rect - end - end - conditions = collect(values) - elements = map(conditions) do c - return MarkerElement(marker = marker_lookup(c), color = marker_color_lookup(c)) - end - return S.Legend(elements, conditions) -end - -widget_value(w::Vector{<:String}; resolution = 1) = w -widget_value(x::Vector; resolution = 1) = - x[1] ≈ x[end] ? Float64[] : range(Float64(x[1]), Float64(x[end]), length = 5) - """ formular_widgets(variables) Creates widgets to control each variable of a model.\\ + Arguments:\\ - `variables::Vector{Pair{Symbol}}` - vector of key-value pairs with information about the model formula terms. @@ -126,52 +95,3 @@ function yhats_signal(model, widget_signal, channel) return yhats_signal end - - -""" - create_plot!(plots, data, vars, scatter_styles, line_styles, continuous_vars) - -Arguments:\\ -- `plots` - a SpecApi list to push into.\\ -- `data` - a DataFrame to be subsetted.\\ -- `vars` contains the levels to be plotted.\\ - -**Return Value:** . -""" -function create_plot!(plots, data, vars, scatter_styles, line_styles, continuous_vars) - - selector = [(name => x -> x .== var) for (name, var) in vars] - - sub = subset(data, selector...) - @assert !isempty(sub) "this shouldn't be empty..." - points = Point2f.(sub.time, sub.yhat) - points[sub.time.≈maximum(sub.time)] .= Ref(Point2f(NaN)) # terrible hack, it will remove the last point from ploitting. better would be to loop the lines! with views of the dataframe... - - - # @debug "create_plot!" scatter_styles vars - #args = [kw => vals[val] for (val, (name, (kw, vals))) in zip(vars, scatter_styles)] - args = [ - scatter_styles[term][1] => scatter_styles[term][2][val] for - (term, val) in vars if term ∈ keys(scatter_styles) - ] - - if isempty(line_styles) - line_args = [] - line_args2 = [] - line_args3 = args - - if !isempty(args) && !any(x -> x[1] .== :color, line_args3) - push!(args, :color => :black) - end - - - else - line_args = [kw => cmap for (name, (kw, (lims, cmap))) in line_styles] - line_args2 = [:colorrange => lims for (name, (kw, (lims, cmap))) in line_styles] - line_args3 = [:color => sub[!, name] for name in continuous_vars] - end - push!(plots, S.Scatter(points; markersize = 10, args...)) - - push!(plots, S.Lines(points; line_args..., line_args2..., line_args3...)) - return -end diff --git a/src/plot_data.jl b/src/functions_plotting.jl similarity index 64% rename from src/plot_data.jl rename to src/functions_plotting.jl index b01d5cc..7f6cee1 100644 --- a/src/plot_data.jl +++ b/src/functions_plotting.jl @@ -3,9 +3,6 @@ plot_data(data, value_ranges, categorical_vars, continuous_vars, mapping_obs) Plotting an interactive dashboard. -Action: -- Create default palletes for colors, markers, line styles and colorstyles for continuous values. - Arguments: - `data::DataFrame` - the result of `effects(Dict(...), model) ` with columns: yhat, channel, dummy, time, eventname and unique columns for each formula term.. - `value_ranges::Vector{Pair{Symbol}}` - value range for continuous variables, levels for categorical. @@ -13,6 +10,13 @@ Action: - `continuous_vars::Vector{Symbol}` - continuous terms. - `mapping::Dict{Symbol, Symbol}` - dictionary with dropdown menus and their default values. +Action: +- Create default palettes for colors, markers, line styles, and color styles for continuous values. +- Check that the terms are not empty. +- Plot the dashboard. +- Define line and scatter styles for the line plot. +- Add line and scatter styles to the legend. + **Return Value:** `Makie.GridLayoutSpec`. """ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) @@ -34,6 +38,7 @@ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) # define what is mapped according to what for categorical scatter_styles = Dict() + for (vals, cat) in zip(cat_levels, cat_terms) if !cat_active[cat] continue @@ -47,6 +52,7 @@ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) end end end + continuous_values = [extrema(data[!, con]) for con in continuous_vars] if isempty(continuous_vars) # if no continuous variable, use the scatter-color for plotting @@ -81,16 +87,15 @@ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) subdata = data subdata = col_term == :none ? data : - subset(data, col_term => level -> level .== col_level) + subset(data, col_term => level -> level .== col_level) # Why subdata is overwritten? subdata = row_term == :none ? subdata : subset(subdata, row_term => level -> level .== row_level) - active_cat_vars = Dict( term => level for (term, level) in zip(cat_terms, cat_levels) if cat_active[term] - )# & + ) if row_term != :none active_cat_vars[row_term] = [row_level] end @@ -102,7 +107,7 @@ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) continue end # create a new term => values (e.g. animal => [fish,cow] ) Dict - create_plot!( + define_scatter_line_style!( plots, subdata, Dict(collect(keys(active_cat_vars)) .=> level_grid), @@ -130,3 +135,64 @@ function plot_data(data, value_ranges, cat_terms, continuous_vars, mapping_obs) return S.GridLayout([(1, 1) => S.GridLayout(axes), (:, 2) => S.GridLayout(legends)]) end end + + +""" + define_scatter_line_style!(plots, data, vars, scatter_styles, line_styles, continuous_vars) +Define styling of lines and points (scatter). + +- subset the data. +- select points and plot scatter. Define scatter style: markersize and color. +- plot lines and define line style: colormap, color range, color. + +Arguments:\\ +- `plots::Vector{Makie.PlotSpec}` - an empty SpecApi list to push into parts of the layout.\\ +- `data::DataFrame` - a DataFrame with predicted values to be subsetted.\\ +- `vars::Dict{Any, Any}` contains the levels to be plotted.\\ +- `scatter_styles::Dict{Any, Any}` - define colors of scatter. +- `line_styles:: Dict{Symbol, Pair{Symbol, Tuple{Tuple{String, String}, Symbol}}}` - define line styles: colormap, color range, color. +- `continuous_vars::Vector{Symbol}` - continuous terms. + +**Return Value:** `Makie.GridLayoutSpec`. +""" +function define_scatter_line_style!( + plots, + data, + vars, + scatter_styles, + line_styles, + continuous_vars, +) + selector = [(name => x -> x .== var) for (name, var) in vars] + + sub = subset(data, selector...) # but len(data) and len(sub) are equal... + + @assert !isempty(sub) "this shouldn't be empty..." + points = Point2f.(sub.time, sub.yhat) + points[sub.time.≈maximum(sub.time)] .= Ref(Point2f(NaN)) # terrible hack, it will remove the last point from ploitting. better would be to loop the lines! with views of the dataframe... + + args = [ + scatter_styles[term][1] => scatter_styles[term][2][val] for + (term, val) in vars if term ∈ keys(scatter_styles) + ] + + if isempty(line_styles) + line_args = [] + line_args2 = [] + line_args3 = args + + if !isempty(args) && !any(x -> x[1] .== :color, line_args3) + push!(args, :color => :black) + end + + else + line_args = [kw => cmap for (name, (kw, (lims, cmap))) in line_styles] + line_args2 = [:colorrange => lims for (name, (kw, (lims, cmap))) in line_styles] + line_args3 = [:color => sub[!, name] for name in continuous_vars] + @debug line_args + end + push!(plots, S.Scatter(points; markersize = 10, args...)) + + push!(plots, S.Lines(points; line_args..., line_args2..., line_args3...)) + return +end diff --git a/src/formula_extractor.jl b/src/functions_preprocessing.jl similarity index 100% rename from src/formula_extractor.jl rename to src/functions_preprocessing.jl diff --git a/src/widgets.jl b/src/widgets.jl deleted file mode 100644 index d0ef3c0..0000000 --- a/src/widgets.jl +++ /dev/null @@ -1,270 +0,0 @@ -struct SelectSet - items::Observable{Vector{Any}} - value::Observable{Vector{Any}} -end - -function SelectSet(items) - return SelectSet( - Base.convert(Observable{Vector{Any}}, items), - Base.convert(Observable{Vector{Any}}, items), - ) -end - -function Bonito.jsrender(s::Session, selector::SelectSet) - rows = map(selector.items[]) do value - c = Bonito.Checkbox(true; class = "p-1 m-1") - on(s, c.value) do x - values = selector.value[] - has_item = (value in values) - if x - !has_item && push!(values, value) - else - has_item && filter!(x -> x != value, values) - end - notify(selector.value) - end - return Row(value, c) - end - return Bonito.jsrender(s, Card(Col(rows...))) -end - -function value_range(args) - type = args[end-1] - default_values = args[end] - if (type == :ContinuousTerm) || (type == :BSplineTerm) - mini = round(Int, default_values.min) - maxi = round(Int, default_values.max) - return range(mini, maxi, length = 5) - elseif type == :CategoricalTerm - return Set(default_values) - else - error("No widget for $(args)") - end -end - -function widget(range::AbstractRange{<:Number}) - - range_slider = RangeSlider(range; value = Any[minimum(range), maximum(range)]) - - range_slider.ticks[] = Dict("mode" => "range", "density" => 10) - range_slider.orientation[] = Bonito.WidgetsBase.vertical - return range_slider -end - -""" - mapping_dropdowns(varnames, var_types) -Map and arrange dropdown menus on the left panel of the dashboard.\\ - -Arguments:\\ -- `varnames::Vector{Symbol}` - vector of the model formula terms. -- `var_types::Vector{Symbol}` - vector of types of the model formula terms. - -Actions: -- Take categorical variables and put their values into dropdown menus. -- There will be 5 menus for: color, markers, line styles, column and row facets. -- Map each menu object with its name on the Figure. -- Create HTML containers using Document Object Model (DOM) from Bonito. -- Arrange containers on the panel using Col() and Row(). Specify their styling. - -**Return Values:** -- `mapping::Observable{Dict{Symbol, Symbol}}` - interactive dictionary with menus and their default value. -- `mapping_dom::Hyperscript.Node{Hyperscript.HTMLSVG}` - dropdown menus in HTML code with styling and layout. -""" -function mapping_dropdowns(varnames, var_types) - cats = [v for (ix, v) in enumerate(varnames) if var_types[ix] == :CategoricalTerm] - push!(cats, :none) - - c_dropdown = Dropdown(cats; index = 1) - m_dropdown = Dropdown(cats; index = length(cats) - 1) - l_dropdown = Dropdown(cats; index = length(cats)) - col_dropdown = Dropdown(cats; index = length(cats)) - row_dropdown = Dropdown(cats; index = length(cats)) - - mapping = @lift Dict( - :color => $(c_dropdown.value), - :marker => $(m_dropdown.value), - :linestyle => $(l_dropdown.value), - :col => $(col_dropdown.value), - :row => $(row_dropdown.value), - ) - mapping_dom = Col( - Row(DOM.div("color:"), c_dropdown, align_items = "center", justify_items = "end"), - Row(DOM.div("marker:"), m_dropdown, align_items = "center", justify_items = "end"), - Row( - DOM.div("linestyle (bug):"), - l_dropdown, - align_items = "center", - justify_items = "end", - ), - Row( - DOM.div("column facet"), - col_dropdown, - align_items = "center", - justify_items = "end", - ), - Row( - DOM.div("row facet"), - row_dropdown, - align_items = "center", - justify_items = "end", - ), - ) - return mapping, mapping_dom - -end -function widget(values::Set) - return SelectSet(collect(values)) -end - -function formular_text(content; class = "") - return DOM.div(content; class = "px-1 text-lg m-1 font-semibold $(class)") -end - -function dropdown(name, content) - return DOM.div( - formular_text(name), - DOM.div(content; class = "dropdown-content"); - class = " bg-slate-100 hover:bg-lime-100 dropdown", - ) -end - -function style_map(::AbstractRange{<:Number}) - return Dict( - :color => identity, - :colormap => RGBAf.(Colors.color.(to_colormap(:lighttest)), 0.5), - ) -end - -function style_map(values::Set) - mpalette = [:circle, :star4, :xcross, :diamond] - dict = Dict(v => mpalette[i] for (i, v) in enumerate(values)) - mcmap = Makie.wong_colors(0.5) - mcolor_lookup = Dict(v => mcmap[i] for (i, v) in enumerate(values)) - return Dict(:marker => v -> dict[v], :marker_color => mcolor_lookup) -end - -function select_vspan(scene; blocking = false, priority = 2, kwargs...) - key = Mouse.left - waspressed = Observable(false) - rect = Observable(Rectf(0, 0, 1, 1)) # plotted rectangle - rect_ret = Observable(Rectf(0, 0, 1, 1)) # returned rectangle - - # Create an initially hidden rectangle - low = Observable(0.0f0) - high = Observable(0.0f0) - - on(rect) do r - low.val = r.origin[1] - high[] = r.origin[1] + r.widths[1] - end - plotted_span = vspan!( - scene, - low, - high, - visible = false, - kwargs..., - transparency = true, - color = (:black, 0.1), - ) - - on(events(scene).mousebutton, priority = priority) do event - if event.button == key - if event.action == Mouse.press && is_mouseinside(scene) - mp = mouseposition(scene) - waspressed[] = true - plotted_span[:visible] = true # start displaying - rect[] = Rectf(mp, 0.0, 0.0) - return Consume(blocking) - end - end - if !(event.button == key && event.action == Mouse.press) - if waspressed[] # User has selected the rectangle - waspressed[] = false - r = Makie.absrect(rect[]) - w, h = widths(r) - rect_ret[] = r - end - # always hide if not the right key is pressed - #plotted_span[:visible] = false # make the plotted rectangle invisible - return Consume(blocking) - end - - return Consume(false) - end - on(events(scene).mouseposition, priority = priority) do event - if waspressed[] - mp = mouseposition(scene) - mini = minimum(rect[]) - rect[] = Rectf(mini, mp - mini) - return Consume(blocking) - end - return Consume(false) - end - - return rect_ret, plotted_span -end - -function rectselect(ax) - selrect, h = select_vspan(ax.scene; color = (0.9)) - translate!(h, 0, 0, -1) # move to background - return selrect -end - -""" - topoplot_widget(positions; size = ()) -Controls the topoplot in the lower left panel of the figure.\\ -Highlight the location of the current electrode and allows electrode selection. - -Arguments:\\ -- `positions::Vector{Point{2, Float32}}` - x an y coordinates of the channels. -- `size::Tuple{Float64, Float64}` - size of the topoplot panel. - -Actions: -- Create interactive scatter. -- Highlight the selected electrode with white color, others are grayed out. -- Create a topolot with a null interpolator and define its style and behavior. -- Hide decorations and spines. - -**Return Values:** -- `h_topo::Makie.FigureAxisPlot` - topoplot widget. -- `interactive_scatter::Observable{Int64}` - number of the selected channel. -""" -function topoplot_widget(positions; size = ()) - strokecolor = Observable(repeat([:red], length(to_value(positions)))) # crashing - interactive_scatter = Observable(1) - - colorrange = vcat(0, 1) - colormap = vcat(Gray(0.5), Gray(1)) - - data_obs = Observable(zeros(length(to_value(positions)))) - data_obs.val[1] = 1 - - h_topo = eeg_topoplot( - data_obs, - nothing; - positions = positions, - colorrange = colorrange, - colormap = colormap, - interpolation = UnfoldMakie.NullInterpolator(), - figure = (; size = size), - axis = (; xzoomlock = true, yzoomlock = true, xrectzoom = false, yrectzoom = false), - label_scatter = (; strokecolor = :black, strokewidth = 1.0, markersize = 20.0), - ) - - on(events(h_topo).mousebutton) do event - if event.button == Mouse.left && event.action == Mouse.press - plt, p = pick(h_topo) - if isa(plt, Makie.Scatter) - data_obs[] .= 0 - data_obs[][p] = 1 - notify(data_obs) - interactive_scatter[] = p - end - - end - end - hidedecorations!(h_topo.axis) - hidespines!(h_topo.axis) - return h_topo, interactive_scatter - -end diff --git a/src/widgets_long.jl b/src/widgets_long.jl new file mode 100644 index 0000000..00c06b3 --- /dev/null +++ b/src/widgets_long.jl @@ -0,0 +1,123 @@ + + +""" + mapping_dropdowns(varnames, var_types) +Map and arrange dropdown menus on the left panel of the dashboard.\\ + +Arguments:\\ +- `varnames::Vector{Symbol}` - vector of the model formula terms. +- `var_types::Vector{Symbol}` - vector of types of the model formula terms. + +Actions: +- Take categorical variables and put their values into dropdown menus. +- There will be 5 menus for: color, markers, line styles, column and row facets. +- Map each menu object with its name on the Figure. +- Create HTML containers using Document Object Model (DOM) from Bonito. +- Arrange containers on the panel using Col() and Row(). Specify their styling. + +**Return Values:** +- `mapping::Observable{Dict{Symbol, Symbol}}` - interactive dictionary with menus and their default value. +- `mapping_dom::Hyperscript.Node{Hyperscript.HTMLSVG}` - dropdown menus in HTML code with styling and layout. +""" +function mapping_dropdowns(varnames, var_types) + cats = [v for (ix, v) in enumerate(varnames) if var_types[ix] == :CategoricalTerm] + push!(cats, :none) + + c_dropdown = Dropdown(cats; index = 1) + m_dropdown = Dropdown(cats; index = length(cats) - 1) + l_dropdown = Dropdown(cats; index = length(cats)) + col_dropdown = Dropdown(cats; index = length(cats)) + row_dropdown = Dropdown(cats; index = length(cats)) + + mapping = @lift Dict( + :color => $(c_dropdown.value), + :marker => $(m_dropdown.value), + :linestyle => $(l_dropdown.value), + :col => $(col_dropdown.value), + :row => $(row_dropdown.value), + ) + mapping_dom = Col( + Row(DOM.div("color:"), c_dropdown, align_items = "center", justify_items = "end"), + Row(DOM.div("marker:"), m_dropdown, align_items = "center", justify_items = "end"), + Row( + DOM.div("linestyle (bug):"), + l_dropdown, + align_items = "center", + justify_items = "end", + ), + Row( + DOM.div("column facet"), + col_dropdown, + align_items = "center", + justify_items = "end", + ), + Row( + DOM.div("row facet"), + row_dropdown, + align_items = "center", + justify_items = "end", + ), + ) + return mapping, mapping_dom + +end + + +""" + topoplot_widget(positions; size = ()) +Controls the topoplot in the lower left panel of the figure.\\ +Highlight the location of the current electrode and allows electrode selection. + +Arguments:\\ +- `positions::Vector{Point{2, Float32}}` - x an y coordinates of the channels. +- `size::Tuple{Float64, Float64}` - size of the topoplot panel. + +Actions: +- Create interactive scatter. +- Highlight the selected electrode with white color, others are grayed out. +- Create a topolot with a null interpolator and define its style and behavior. +- Hide decorations and spines. + +**Return Values:** +- `h_topo::Makie.FigureAxisPlot` - topoplot widget. +- `interactive_scatter::Observable{Int64}` - number of the selected channel. +""" +function topoplot_widget(positions; size = ()) + strokecolor = Observable(repeat([:red], length(to_value(positions)))) # crashing + interactive_scatter = Observable(1) + + colorrange = vcat(0, 1) + colormap = vcat(Gray(0.5), Gray(1)) + + data_obs = Observable(zeros(length(to_value(positions)))) + data_obs.val[1] = 1 + + h_topo = eeg_topoplot( + data_obs, + nothing; + positions = positions, + colorrange = colorrange, + colormap = colormap, + interpolation = UnfoldMakie.NullInterpolator(), + figure = (; size = size), + axis = (; xzoomlock = true, yzoomlock = true, xrectzoom = false, yrectzoom = false), + label_scatter = (; strokecolor = :black, strokewidth = 1.0, markersize = 20.0), + ) + + on(events(h_topo).mousebutton) do event + if event.button == Mouse.left && event.action == Mouse.press + plt, p = pick(h_topo) + if isa(plt, Makie.Scatter) + data_obs[] .= 0 + data_obs[][p] = 1 + notify(data_obs) + interactive_scatter[] = p + end + + end + end + hidedecorations!(h_topo.axis) + hidespines!(h_topo.axis) + return h_topo, interactive_scatter + +end diff --git a/src/widgets_short.jl b/src/widgets_short.jl new file mode 100644 index 0000000..71969ee --- /dev/null +++ b/src/widgets_short.jl @@ -0,0 +1,50 @@ +struct SelectSet + items::Observable{Vector{Any}} + value::Observable{Vector{Any}} +end + +function SelectSet(items) + return SelectSet( + Base.convert(Observable{Vector{Any}}, items), + Base.convert(Observable{Vector{Any}}, items), + ) +end + +function value_range(args) + type = args[end-1] + default_values = args[end] + if (type == :ContinuousTerm) || (type == :BSplineTerm) + mini = round(Int, default_values.min) + maxi = round(Int, default_values.max) + return range(mini, maxi, length = 5) + elseif type == :CategoricalTerm + return Set(default_values) + else + error("No widget for $(args)") + end +end + +function dropdown(name, content) + return DOM.div( + formular_text(name), + DOM.div(content; class = "dropdown-content"); + class = " bg-slate-100 hover:bg-lime-100 dropdown", + ) +end + +function widget(values::Set) + return SelectSet(collect(values)) +end + +function widget(range::AbstractRange{<:Number}) + + range_slider = RangeSlider(range; value = Any[minimum(range), maximum(range)]) + + range_slider.ticks[] = Dict("mode" => "range", "density" => 10) + range_slider.orientation[] = Bonito.WidgetsBase.vertical + return range_slider +end + +function formular_text(content; class = "") + return DOM.div(content; class = "px-1 text-lg m-1 font-semibold $(class)") +end