diff --git a/docs/literate/reference/positions.jl b/docs/literate/explanations/positions.jl similarity index 100% rename from docs/literate/reference/positions.jl rename to docs/literate/explanations/positions.jl diff --git a/docs/literate/how_to/hide_deco.jl b/docs/literate/how_to/hide_deco.jl index 4f9f7dbd..86b26b6a 100644 --- a/docs/literate/how_to/hide_deco.jl +++ b/docs/literate/how_to/hide_deco.jl @@ -28,7 +28,7 @@ plot_butterfly!( data; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, axis = (; title = "With decorations"), ) @@ -37,7 +37,7 @@ plot_butterfly!( data; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, axis = (; title = "Without decorations"), layout = (; hidedecorations = (:label => true, :ticks => true, :ticklabels => true)), diff --git a/docs/literate/how_to/mult_vis_in_fig.jl b/docs/literate/how_to/mult_vis_in_fig.jl index 27f1c949..31a32118 100644 --- a/docs/literate/how_to/mult_vis_in_fig.jl +++ b/docs/literate/how_to/mult_vis_in_fig.jl @@ -83,8 +83,8 @@ plot_designmatrix!(f[2, 3], designmatrix(uf)) plot_topoplot!(f[3, 1], data[:, 150, 1]; positions = positions) plot_topoplotseries!( f[4, 1:3], - d_topo, - 0.1; + d_topo; + bin_width = 0.1, positions = positions, mapping = (; label = :channel), ) @@ -157,7 +157,7 @@ plot_butterfly!( d_topo; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, ) hlines!(0, color = :gray, linewidth = 1) @@ -166,8 +166,8 @@ plot_topoplot!(gc, data[:, 340, 1]; positions = positions, axis = (; xlabel = "[ plot_topoplotseries!( gd, - df, - 80; + df; + bin_width = 80, positions = positions, visual = (label_scatter = false,), layout = (; use_colorbar = true), diff --git a/docs/literate/how_to/position2color.jl b/docs/literate/how_to/position2color.jl index bdb05f0b..247d3a1f 100644 --- a/docs/literate/how_to/position2color.jl +++ b/docs/literate/how_to/position2color.jl @@ -54,3 +54,19 @@ plot_butterfly( positions = positions, topopositions_to_color = x -> Colors.RGB(0.5), ) + +# Transparency +# Unlike RGB, RGBA has a fourth channel, alpha, which is responsible for transparency. +# Here are two examples of how to manipulate it. + +plot_butterfly( + results; + positions = positions, + topopositions_to_color = x -> (RGBA(UnfoldMakie.pos_to_color_RomaO(x), 1)), +) + +plot_butterfly( + results; + positions = positions, + topopositions_to_color = x -> (GrayA(UnfoldMakie.pos_to_color_RomaO(x), 0.5)), +) diff --git a/docs/literate/intro/code_principles.jl b/docs/literate/intro/code_principles.jl new file mode 100644 index 00000000..3ac8df4b --- /dev/null +++ b/docs/literate/intro/code_principles.jl @@ -0,0 +1,11 @@ +# # Code principles + + +# Here we will write about principles which we developed through our publication. + +#- Code should be clear and concise +#- Variables inside the code should have meaningful names +#- Every function exposed to the user should have documentation that specifies all parameters, types, input and output arguments. +#- Most people will not look at the defaults, so it is very important to nudge users to show important details with a picture or text. +#- Function naming should be based on some theory and naming conventions. +#- You should avoid functions longer 50 lines diff --git a/docs/literate/intro/plot_types.jl b/docs/literate/intro/plot_types.jl index 851f8e88..3a5fa0dc 100644 --- a/docs/literate/intro/plot_types.jl +++ b/docs/literate/intro/plot_types.jl @@ -1,7 +1,7 @@ # # The Dilemma of Multidimensionality # !!! note -# Please read the paper [The Art of Brainwaves](https://apertureneuro.org/article/116386-the-art-of-brainwaves-a-survey-on-event-related-potential-visualization-practices), if you want to know more about how we come up with these plot types. +# Please read the paper [The Art of Brainwaves](https://apertureneuro.org/article/116386-the-art-of-brainwaves-a-survey-on-event-related-potential-visualization-practices), if you want to know how we come up with these plot types. #= EEG – multidimensional data and could be presented differently. diff --git a/docs/literate/tutorials/butterfly.jl b/docs/literate/tutorials/butterfly.jl index 11c6256e..3e30912f 100644 --- a/docs/literate/tutorials/butterfly.jl +++ b/docs/literate/tutorials/butterfly.jl @@ -1,5 +1,5 @@ # # [Butterfly Plot](@id bfp_vis) -# Butterfly plot is a plot type for visualisation of Event-related potentials. +# **Butterfly plot** is a plot type for visualisation of Event-related potentials. # It can fully represent time and channels dimensions using lines. With addition of topoplot inset it can also represent location of channels. # It called "butterfly" because the envelope of channels reminds butterfly wings🦋. @@ -38,7 +38,7 @@ plot_butterfly(df; positions = pos) # You want to change size of topomarkers and size of topoplot: -plot_butterfly(df; positions = pos, topomarkersize = 10, topoheigth = 0.4, topowidth = 0.4) +plot_butterfly(df; positions = pos, topomarkersize = 10, topoheight = 0.4, topowidth = 0.4) # You want to add vline and hline: diff --git a/docs/literate/tutorials/channel_image.jl b/docs/literate/tutorials/channel_image.jl index 9a56a357..c73a9a7b 100644 --- a/docs/literate/tutorials/channel_image.jl +++ b/docs/literate/tutorials/channel_image.jl @@ -1,6 +1,6 @@ # # Channel image -# Channel image is a plot type for visualizing EEG activity for all channels. +# **Channel image** is a plot type for visualizing EEG activity for all channels. # It can fully represent time and channel dimensions using a heatmap. # Y-axis represents all channels, x-axis represents time, while color represents voltage. diff --git a/docs/literate/tutorials/circ_topo.jl b/docs/literate/tutorials/circ_topo.jl index f6b34d6a..f8be30f5 100644 --- a/docs/literate/tutorials/circ_topo.jl +++ b/docs/literate/tutorials/circ_topo.jl @@ -1,5 +1,10 @@ # # Circular Topoplots +# **Circular topoplot series** is a plot type for visualizing EEG activity in relation to some continous variable arranged on a circluar line. +# It can fully represent channel and channel location dimensions using contour lines. It can also partially represent the varaible dimension. +# Variable could be for instance accadic amplitude or degrees of visual angle. +# Basically, it is a series of Topoplots arranged on a circle. + # # Setup # ## Package loading diff --git a/docs/literate/tutorials/erp.jl b/docs/literate/tutorials/erp.jl index 41d39d12..721063ba 100644 --- a/docs/literate/tutorials/erp.jl +++ b/docs/literate/tutorials/erp.jl @@ -1,6 +1,6 @@ # # [ERP Plot](@id erp_vis) -# ERP plot is plot type for visualisation of [Event-related potentials](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3016705/). +# **ERP plot** is plot type for visualisation of [Event-related potentials](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3016705/). # It can fully represent time and experimental condition dimensions using lines. # # Setup diff --git a/docs/literate/tutorials/erp_grid.jl b/docs/literate/tutorials/erp_grid.jl index 9e36b1e4..0ebfaccc 100644 --- a/docs/literate/tutorials/erp_grid.jl +++ b/docs/literate/tutorials/erp_grid.jl @@ -1,4 +1,8 @@ # # ERP grid +# **ERP grid** is a plot type for visualisation of Event-related potentials. +# It can fully represent time, channel, and layout (channel locations) dimensions using lines. It can also partially represent condition dimensions. +# Lines are displayed on a grid. The location of each axis represents the location of the electrode. +# This plot type is not as popular because it is too cluttered. # # Setup # ## Package loading diff --git a/docs/literate/tutorials/erpimage.jl b/docs/literate/tutorials/erpimage.jl index c8488bfe..9af55605 100644 --- a/docs/literate/tutorials/erpimage.jl +++ b/docs/literate/tutorials/erpimage.jl @@ -1,6 +1,6 @@ # # ERP image -# ERP image is a plot type for visualizing EEG activity for all trials. +# **ERP image** is a plot type for visualizing EEG activity for all trials. # It can fully represent time and trial dimensions using a heatmap. # Y-axis represents all trials, x-axis represents time, while color represents voltage. # The ERP image can also be sorted by specific experimental variables, which helps to reveal important correlations. diff --git a/docs/literate/tutorials/parallelcoordinates.jl b/docs/literate/tutorials/parallelcoordinates.jl index f6241b66..87f3f461 100644 --- a/docs/literate/tutorials/parallelcoordinates.jl +++ b/docs/literate/tutorials/parallelcoordinates.jl @@ -1,6 +1,6 @@ # # Parallel Coordinates -# Parallel Coordinates Plot (PCP) is a plot type used to visualize EEG activity for some channels. +# **Parallel Coordinates Plot** (PCP) is a plot type used to visualize EEG activity for some channels. # It can fully represent state and channel dimensions using lines. It can also partially represent time or trials # Y-axis represents time points, vertical axes represent channels, while lines show voltage changes. diff --git a/docs/literate/tutorials/topoplot.jl b/docs/literate/tutorials/topoplot.jl index 7a87fc1e..8eab2c91 100644 --- a/docs/literate/tutorials/topoplot.jl +++ b/docs/literate/tutorials/topoplot.jl @@ -1,5 +1,5 @@ # # [Topoplot](@id topo_vis) -# Topoplot (aka topography plot) is a plot type for visualisation of EEG activity in a specific time stemp or time interval. +# **Topoplot** (aka topography plot) is a plot type for visualisation of EEG activity in a specific time stemp or time interval. # It can fully represent channel and channel location dimensions using contour lines. # The topoplot is a 2D projection and interpolation of the 3D distributed sensor activity. The name stems from physical geography, but instead of height, the contour lines represent voltage levels. diff --git a/docs/literate/tutorials/topoplotseries.jl b/docs/literate/tutorials/topoplotseries.jl index e51fac1b..03d9f9fb 100644 --- a/docs/literate/tutorials/topoplotseries.jl +++ b/docs/literate/tutorials/topoplotseries.jl @@ -1,6 +1,6 @@ # # Topoplot Series -# Topoplot Series is a plot type for visualizing EEG activity in a given time frame or time interval. +# **Topoplot series** is a plot type for visualizing EEG activity in a given time frame or time interval. # It can fully represent channel and channel location dimensions using contour lines. It can also partially represent the time dimension. # Basically, it is a series of Topoplots. @@ -23,14 +23,14 @@ nothing #hide # # Plotting -Δbin = 80 -plot_topoplotseries(df, Δbin; positions = positions) +bin_width = 80 +plot_topoplotseries(df; bin_width, positions = positions) # # Additional features # ## Disabling colorbar -plot_topoplotseries(df, Δbin; positions = positions, layout = (; use_colorbar = false)) +plot_topoplotseries(df; bin_width, positions = positions, layout = (; use_colorbar = false)) # ## Aggregating functions # In this example `combinefun` is specified by `mean`, `median` and `std`. @@ -38,24 +38,24 @@ plot_topoplotseries(df, Δbin; positions = positions, layout = (; use_colorbar = f = Figure(size = (500, 500)) plot_topoplotseries!( f[1, 1], - df, - Δbin; + df; + bin_width, positions = positions, combinefun = mean, - axis = (; title = "combinefun = mean"), + axis = (; xlabel = "", title = "combinefun = mean"), ) plot_topoplotseries!( f[2, 1], - df, - Δbin; + df; + bin_width, positions = positions, combinefun = median, - axis = (; title = "combinefun = median"), + axis = (; xlabel = "", title = "combinefun = median"), ) plot_topoplotseries!( f[3, 1], - df, - Δbin; + df; + bin_width, positions = positions, combinefun = std, axis = (; title = "combinefun = std"), @@ -66,36 +66,54 @@ f #= If you need to plot many topoplots, you should display them in multiple rows. - -Here you can specify: -- Grouping condition using `mapping.row`. -- Label the y-axis with `axis.ylabel`. -- Hide electrode markers with `visual.label_scatter`. -- Change the color map with `visual.colormap`. The default is `Reverse(:RdBu)`. -- Adjust the limits of the topoplot boxes with `axis.xlim_topo` and `axis.ylim_topo`. By default both are `(-0.25, 1.25)`. -- Adjust the size of the figure with `Figure(size = (x, y))`. -- Adjust the padding between topoplot labels and axis labels using `xlabelpadding` and `ylabelpadding`. =# + +f = Figure() df1 = UnfoldMakie.eeg_matrix_to_dataframe(data[:, :, 1], string.(1:length(positions))) -df1.condition = repeat(["A", "B", "C", "D", "E"], size(df, 1) ÷ 5) +plot_topoplotseries!( + f[1, 1:5], + df1; + bin_num = 14, + nrows = 4, + positions = positions, + visual = (; label_scatter = false), +) +f + +# ## Categorical topoplots + +#= +If you decide to use categorical values instead of time intvervals for sepration of topoplots do this: +- Do not specify `bin_width` or `bin_num` +- Put categorical value in `mapping.col` +=# + +df2 = UnfoldMakie.eeg_matrix_to_dataframe(data[:, 1:5, 1], string.(1:length(positions))) +df2.condition = repeat(["A", "B", "C", "D", "E"], size(df2, 1) ÷ 5) f = Figure(size = (600, 500)) plot_topoplotseries!( - f[1:2, 1:2], - df1, - Δbin; + f[1, 1], + df2; col_labels = true, - mapping = (; row = :condition), - axis = (; ylabel = "Conditions"), + mapping = (; col = :condition), + axis = (; xlabel = "Conditions"), positions = positions, - visual = (label_scatter = false,), - layout = (; use_colorbar = true), ) f # # Configurations of Topoplot series +#= +Also you can specify: +- Label the x-axis with `axis.xlabel`. +- Hide electrode markers with `visual.label_scatter`. +- Change the color map with `visual.colormap`. The default is `Reverse(:RdBu)`. +- Adjust the limits of the topoplot boxes with `axis.xlim_topo` and `axis.ylim_topo`. By default both are `(-0.25, 1.25)`. +- Adjust the size of the figure with `Figure(size = (x, y))`. +- Adjust the padding between topoplot labels and axis labels using `xlabelpadding` and `ylabelpadding`. +=# # ```@docs # plot_topoplotseries # ``` diff --git a/docs/make.jl b/docs/make.jl index bb67fb2e..015759cf 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,7 +15,7 @@ using Glob GENERATED = joinpath(@__DIR__, "src", "generated") SOURCE = joinpath(@__DIR__, "literate") -for subfolder ∈ ["how_to", "intro", "tutorials", "reference"] +for subfolder ∈ ["how_to", "intro", "tutorials", "explanations"] local SOURCE_FILES = Glob.glob(subfolder * "/*.jl", SOURCE) foreach(fn -> Literate.markdown(fn, GENERATED * "/" * subfolder), SOURCE_FILES) end @@ -38,6 +38,7 @@ makedocs(; "Intro" => [ "Installation" => "generated/intro/installation.md", "Plot types" => "generated/intro/plot_types.md", + "Code principles" => "generated/intro/code_principles.md", ], "Visualization Types" => [ "ERP plot" => "generated/tutorials/erp.md", @@ -56,8 +57,8 @@ makedocs(; "Hide Decorations and Axis Spines" => "generated/how_to/hide_deco.md", "Include multiple figures in one" => "generated/how_to/mult_vis_in_fig.md", ], - "Reference" => [ - "Convert electrode positions from 3D to 2D" => "generated/reference/positions.md", + "Explanations" => [ + "Convert electrode positions from 3D to 2D" => "generated/explanations/positions.md", ], "API / DocStrings" => "api.md", "Utilities" => "helper.md", diff --git a/docs/src/index.md b/docs/src/index.md index 4a9615c6..0aae8e04 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,17 +4,19 @@ ``` -This is the documentation of the UnfoldMakie.jl module (aka library) for the Julia programming language. +This is the documentation of the UnfoldMakie.jl package for the Julia programming language. -## Benefits of UnfoldMakie.jl +## Highlights of UnfoldMakie.jl - **10 plot functions for displaying ERPs.** Each plot emphasizes certain dimensions while collapsing others. +- **Fast plotting** +Plot one figure with 20 topoplots in 1 second? No problemo! - **Highly adaptable.** -The module is based on the [Unfold](https://github.com/unfoldtoolbox/unfold.jl/) and [Makie](https://makie.juliaplots.org/stable/) modules, so you can use configurations from these modules to add new features to your figures. +The package is primarily based on [Unfold.jl](https://github.com/unfoldtoolbox/unfold.jl/) and [Makie.jl](https://makie.juliaplots.org/stable/). - **Many usage examples** -Here in documentation you can find user-friendly examples of how to use plots. -- **Scientific colormaps as default** -According to our study (Mikheev, 2024), 40% of EEG researchers do not know about the issue of scientific color maps. To protect the scientific integrity, we used `Reverse(:RdBu)` and `Roma` as default color maps. +You can find many user-friendly examples of how to use and adapt the plots in this documentation. +- **Scientific colormaps by default** +According to our study [(Mikheev, 2024)](https://apertureneuro.org/article/116386-the-art-of-brainwaves-a-survey-on-event-related-potential-visualization-practices),, 40% of EEG researchers do not know about the issue of scientific color maps. By default, we use `Reverse(:RdBu)` (based on [colorbrewer](https://colorbrewer2.org/#type=sequential&scheme=BuGn&n=3)) and `Roma` (based on [Sceintific Colormaps](https://www.fabiocrameri.ch/colourmaps/) by Fabio Crameri) as default color maps. - **Interactivity** -Several plots use Observables and have interactive mode so you can click on them and change their layout. Check `plot_topoplotseries` and `plot_erpimage`. \ No newline at end of file +Several plots make use of `Observables.jl` which allows fast updating of the underlying data. Several plots already have predfined interactive features, e.g. you can click on labels to enable / disable them. See `plot_topoplotseries` and `plot_erpimage` for examples. diff --git a/src/eeg_series.jl b/src/eeg_series.jl index 0350ce51..48489fa3 100644 --- a/src/eeg_series.jl +++ b/src/eeg_series.jl @@ -16,9 +16,10 @@ end """ eeg_topoplot_series(data::DataFrame, - fig, - data::DataFrame, - Δbin; + f, + data::DataFrame; + bin_width, + bin_num, y = :erp, label = :label, col = :time, @@ -31,16 +32,16 @@ end ylim_topo, topoplot_attributes..., ) - eeg_topoplot_series!(fig, data::DataFrame, Δbin; kwargs..) + eeg_topoplot_series!(fig, data::DataFrame, bin_width; kwargs..) Plot a series of topoplots. -The function automatically takes the `combinefun = mean` over the `:time` column of `data` in `Δbin` steps. -- `fig` \\ +The function automatically takes the `combinefun = mean` over the `:time` column of `data` in `bin_width` steps. +- `f` \\ Figure object. \\ - `data::DataFrame`\\ Needs the columns `:time` and `y(=:erp)`, and `label(=:label)`. \\ If `data` is a matrix, it is automatically cast to a dataframe, time bins are in samples, labels are `string.(1:size(data,1))`. -- `Δbin = :time` \\ +- `bin_width = :time` \\ In `:time` units, specifying the time steps. All other keyword arguments are passed to the `EEG_TopoPlot` recipe. \\ In most cases, the user should specify the electrode positions with `positions = pos`. - `col`, `row = :time` \\ @@ -61,36 +62,47 @@ eeg_topoplot_series(df, 5; positions = pos) **Return Value:** `Tuple{Figure, Vector{Any}}`. """ function eeg_topoplot_series( - data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}, - Δbin; + data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}; figure = NamedTuple(), kwargs..., ) - return eeg_topoplot_series!(Figure(; figure...), data, Δbin; kwargs...) + return eeg_topoplot_series!(Figure(; figure...), data; kwargs...) end -# allow to specify Δbin as an keyword for nicer readability -eeg_topoplot_series( +# allow to specify bin_width as an keyword for nicer readability + +#eeg_topoplot_series(data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}; kwargs...) = +# eeg_topoplot_series(data; kwargs...) +# AbstractMatrix +function eeg_topoplot_series!( + fig, data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}; - Δbin, kwargs..., -) = eeg_topoplot_series(data, Δbin; kwargs...) -# AbstractMatrix -function eeg_topoplot_series!(fig, data::AbstractMatrix, Δbin; kwargs...) - return eeg_topoplot_series!(fig, data, string.(1:size(data, 1)), Δbin; kwargs...) +) + return eeg_topoplot_series!(fig, data, string.(1:size(data, 1)); kwargs...) end # convert a 2D Matrix to the dataframe -function eeg_topoplot_series(data::AbstractMatrix, labels, Δbin; kwargs...) - return eeg_topoplot_series(eeg_matrix_to_dataframe(data, labels), Δbin; kwargs...) +function eeg_topoplot_series( + data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}, + labels; + kwargs..., +) + return eeg_topoplot_series(eeg_matrix_to_dataframe(data, labels); kwargs...) end -function eeg_topoplot_series!(fig, data::AbstractMatrix, labels, Δbin; kwargs...) - return eeg_topoplot_series!(fig, eeg_matrix_to_dataframe(data, labels), Δbin; kwargs...) +function eeg_topoplot_series!( + fig, + data::Union{<:Observable,<:DataFrame,<:AbstractMatrix}, + labels; + kwargs..., +) + return eeg_topoplot_series!(fig, eeg_matrix_to_dataframe(data, labels); kwargs...) end function eeg_topoplot_series!( fig, - data::Union{<:Observable{<:DataFrame},<:DataFrame}, - Δbin; + data::Union{<:Observable{<:DataFrame},<:DataFrame}; + bin_width = nothing, + bin_num = nothing, y = :erp, label = :label, col = :time, @@ -102,47 +114,20 @@ function eeg_topoplot_series!( xlim_topo = (-0.25, 1.25), ylim_topo = (-0.25, 1.25), interactive_scatter = nothing, - highlight_scatter = false,#Observable([0]), + highlight_scatter = false, topoplot_attributes..., ) - - # cannot be made easier right now, but Simon promised a simpler solution "soonish" - axisOptions = ( - aspect = 1, - xgridvisible = false, - xminorgridvisible = false, - xminorticksvisible = false, - xticksvisible = false, - xticklabelsvisible = false, - xlabelvisible = false, - ygridvisible = false, - yminorgridvisible = false, - yminorticksvisible = false, - yticksvisible = false, - yticklabelsvisible = false, - ylabelvisible = false, - leftspinevisible = false, - rightspinevisible = false, - topspinevisible = false, - bottomspinevisible = false, - xpanlock = true, - ypanlock = true, - xzoomlock = true, - yzoomlock = true, - xrectzoom = false, - yrectzoom = false, - limits = (xlim_topo, ylim_topo), - ) + axis_options = create_axis_options(xlim_topo, ylim_topo) # aggregate the data over time bins # using same colormap + contour levels for all plots data = _as_observable(data) if eltype(to_value(data)[!, col]) <: Number - data_mean = @lift( df_timebin( - $data, - Δbin; + $data; + bin_width = bin_width, + bin_num = bin_num, col_y = y, fun = combinefun, grouping = [label, col, row], @@ -157,13 +142,12 @@ function eeg_topoplot_series!( ( colorrange = (q_min, q_max), interp_resolution = (128, 128), - contours = (levels = range(q_min, q_max; length = 7),), + contours = (levels = range(q_min, q_max; length = 7)), ), topoplot_attributes, ) # do the col/row plot - select_col = isnothing(col) ? 1 : unique(to_value(data_mean)[:, col]) select_row = isnothing(row) ? 1 : unique(to_value(data_mean)[:, row]) @@ -171,126 +155,160 @@ function eeg_topoplot_series!( @assert isa(interactive_scatter, Observable) end - axlist = [] for r = 1:length(select_row) for c = 1:length(select_col) - ax = Axis(fig[:, :][r, c]; axisOptions...) - # select one topoplot - sel = 1 .== ones(size(to_value(data_mean), 1)) # select all - if !isnothing(col) - sel = sel .&& (to_value(data_mean)[:, col] .== select_col[c]) # subselect - end - if !isnothing(row) - sel = sel .&& (to_value(data_mean)[:, row] .== select_row[r]) # subselect - end - - df_single = @lift($data_mean[sel, :]) - - # select labels - labels = to_value(df_single)[:, label] - # select data - d_vec = @lift($df_single[:, y]) - # plot it - if highlight_scatter != false || interactive_scatter != nothing - - # pos = @lift topoplot_attributes[:positions][highlight_scatter] - strokecolor = Observable(repeat([:black], length(to_value(d_vec)))) - - - highlight_feature = (; strokecolor = strokecolor) - if :label_scatter ∈ keys(topoplot_attributes) - topoplot_attributes = merge( - topoplot_attributes, - (; - label_scatter = merge( - topoplot_attributes[:label_scatter], - highlight_feature, - ) - ), - ) - else - topoplot_attributes = - merge(topoplot_attributes, (; label_scatter = highlight_feature)) - end - - - end - h_topo = eeg_topoplot!(ax, d_vec, labels; topoplot_attributes...) - @debug typeof(h_topo) typeof(ax) - - if rasterize_heatmaps - h_topo.plots[1].plots[1].rasterize = true - end - if r == length(select_row) && col_labels - ax.xlabel = string(to_value(df_single)[1, col]) - ax.xlabelvisible = true - end - if c == 1 && length(select_row) > 1 && row_labels - #@show df_single - ax.ylabel = string(to_value(df_single)[1, row]) - ax.ylabelvisible = true - end - - if interactive_scatter != false - - 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) && plt == h_topo.plots[1].plots[3] - - plt.strokecolor[] .= :black - plt.strokecolor[][p] = :white - notify(plt.strokecolor) # not sure why this is necessary, but oh well.. - - interactive_scatter[] = (r, c, p) - end - - end - end - end - + ax = single_topoplot( + fig, + r, + c, + row, + col, + select_row, + select_col, + y, + label, + axis_options, + data_mean, + highlight_scatter, + interactive_scatter, + topoplot_attributes, + col_labels, + row_labels, + rasterize_heatmaps, + ) push!(axlist, ax) + end end if typeof(fig) != GridLayout && typeof(fig) != GridLayoutBase.GridSubposition colgap!(fig.layout, 0) end - return fig, axlist end -""" - df_timebin(df, Δbin; col_y = :erp, fun = mean, grouping = []) -Split or combine `DataFrame` according to equally spaced time bins. - -Arguments: -- `df::AbstractTable`\\ - With columns `:time` and `col_y` (default `:erp`), and all columns in `grouping`; -- `Δbin`\\ - Bin size in `:time` units; -- `col_y = :erp` \\ - The column to combine over (with `fun`); -- `fun = mean()`\\ - Function to combine. -- `grouping = []`\\ - Vector of symbols or strings, columns to group the data by before aggregation. Values of `nothing` are ignored. - -**Return Value:** `DataFrame`. -""" -function df_timebin(df, Δbin; col_y = :erp, fun = mean, grouping = []) - tmin = minimum(df.time) - tmax = maximum(df.time) +function single_topoplot( + fig, + r, + c, + row, + col, + select_row, + select_col, + y, + label, + axis_options, + data_mean, + highlight_scatter, + interactive_scatter, + topoplot_attributes, + col_labels, + row_labels, + rasterize_heatmaps, +) + ax = Axis(fig[:, :][r, c]; axis_options...) + # select one topoplot + sel = 1 .== ones(size(to_value(data_mean), 1)) # select all + if !isnothing(col) + sel = sel .&& (to_value(data_mean)[:, col] .== select_col[c]) # subselect + end + if !isnothing(row) + sel = sel .&& (to_value(data_mean)[:, row] .== select_row[r]) # subselect + end + df_single = @lift($data_mean[sel, :]) + + # select labels + labels = to_value(df_single)[:, label] + # select data + d_vec = @lift($df_single[:, y]) + # plot it + if highlight_scatter != false || interactive_scatter != nothing + strokecolor = Observable(repeat([:black], length(to_value(d_vec)))) + highlight_feature = (; strokecolor = strokecolor) + + if :label_scatter ∈ keys(topoplot_attributes) + topoplot_attributes = merge( + topoplot_attributes, + (; + label_scatter = if isa(topoplot_attributes[:label_scatter], NamedTuple) + merge(topoplot_attributes[:label_scatter], highlight_feature) + else + highlight_feature + end + ), + ) + else + topoplot_attributes = + merge(topoplot_attributes, (; label_scatter = highlight_feature)) + end + end + if isempty(to_value(d_vec)) + return + end + single_topoplot = eeg_topoplot!(ax, d_vec, labels; topoplot_attributes...) + if rasterize_heatmaps + single_topoplot.plots[1].plots[1].rasterize = true + end - bins = range(; start = tmin, step = Δbin, stop = tmax) - df = deepcopy(df) # cut seems to change stuff inplace - df.time = cut(df.time, bins; extend = true) + # to put column and row labels + if col_labels == true + if r == length(select_row) && col_labels + ax.xlabel = string(to_value(df_single)[1, col]) + ax.xlabelvisible = true + end + if c == 1 && length(select_row) > 1 && row_labels + ax.ylabel = string(to_value(df_single)[1, row]) + ax.ylabelvisible = true + end + else + ax.xlabelvisible = true + ax.xlabel = string(to_value(df_single).time[1, :][]) + end + interctive_toposeries(interactive_scatter, single_topoplot) + return ax +end - grouping = grouping[.!isnothing.(grouping)] +function interctive_toposeries(interactive_scatter, single_topoplot) + if interactive_scatter != false + on(events(single_topoplot).mousebutton) do event + if event.button == Mouse.left && event.action == Mouse.press + plt, p = pick(single_topoplot) + if isa(plt, Makie.Scatter) && plt == single_topoplot.plots[1].plots[3] + plt.strokecolor[] .= :black + plt.strokecolor[][p] = :white + notify(plt.strokecolor) # not sure why this is necessary, but oh well.. + interactive_scatter[] = (r, c, p) + end + end + end + end +end - df_m = combine(groupby(df, unique([:time, grouping...])), col_y => fun) - #df_m = combine(groupby(df, Not(y)), y=>fun) - rename!(df_m, names(df_m)[end] => col_y) # remove the _fun part of the new column - return df_m +function create_axis_options(xlim_topo, ylim_topo) + return ( + aspect = 1, + xgridvisible = false, + xminorgridvisible = false, + xminorticksvisible = false, + xticksvisible = false, + xticklabelsvisible = false, + xlabelvisible = false, + ygridvisible = false, + yminorgridvisible = false, + yminorticksvisible = false, + yticksvisible = false, + yticklabelsvisible = false, + ylabelvisible = false, + leftspinevisible = false, + rightspinevisible = false, + topspinevisible = false, + bottomspinevisible = false, + xpanlock = true, + ypanlock = true, + xzoomlock = true, + yzoomlock = true, + xrectzoom = false, + yrectzoom = false, + limits = (xlim_topo, ylim_topo), + ) end diff --git a/src/layout_helper.jl b/src/layout_helper.jl index 1aeb9fc0..183e27f6 100644 --- a/src/layout_helper.jl +++ b/src/layout_helper.jl @@ -53,14 +53,6 @@ function apply_layout_settings!( if :hidedecorations ∈ keys(config.layout) && !isnothing(config.layout.hidedecorations) hidedecorations!(ax; config.layout.hidedecorations...) end - - # automatic labels - #if !isnothing(config.layout.xlabelFromMapping) - # ax.xlabel = string(config.mapping[config.layout.xlabelFromMapping]) - #end - #if !isnothing(config.layout.ylabelFromMapping) - # ax.ylabel = string(config.mapping[config.layout.ylabelFromMapping]) - #end end Makie.hidedecorations!(ax::Matrix{AxisEntries}; kwargs...) = Makie.hidedecorations!.(ax; kwargs...) diff --git a/src/plot_circular_topoplots.jl b/src/plot_circular_topoplots.jl index e0690a61..a22f4452 100644 --- a/src/plot_circular_topoplots.jl +++ b/src/plot_circular_topoplots.jl @@ -20,13 +20,13 @@ Plot a circular EEG topoplot. Positions of the [`plot_topoplot`](@ref topo_vis). - `center_label::String = ""`\\ The text in the center of the cricle. +- `plot_radius::String = 0.8`\\ + The radius of the circular topoplot series plot calucalted by formula: `radius = (minwidth * plot_radius) / 2`. - `labels::Vector{String} = nothing`\\ Labels for the [`plot_topoplots`](@ref topo_vis). $(_docstring(:circtopos)) - - **Return Value:** `Figure` displaying the Circular topoplot series. """ @@ -42,6 +42,7 @@ function plot_circular_topoplots!( positions = nothing, labels = nothing, center_label = "", + plot_radius = 0.8, kwargs..., ) config = PlotConfig(:circtopos) @@ -89,7 +90,7 @@ function plot_circular_topoplots!( height = @lift Fixed($(pixelarea(ax.scene)).widths[2]) ) plot_topo_plots!( - ax, + f[1, 1], data[:, config.mapping.y], positions, predictor_values, @@ -97,6 +98,7 @@ function plot_circular_topoplots!( min, max, labels, + plot_radius, ) apply_layout_settings!(config; ax = ax) @@ -119,7 +121,6 @@ end function plot_circular_axis!(ax, predictor_bounds, center_label) # The axis position is always the middle of the screen # It uses the GridLayout's full size - lines!( ax, 1 * cos.(LinRange(0, 2 * pi, 500)), @@ -184,33 +185,22 @@ function plot_topo_plots!( globalmin, globalmax, labels, + plot_radius, ) df = DataFrame(:e => data, :p => predictor_values) gp = groupby(df, :p) i = 0 for g in gp i += 1 - bbox = calculate_BBox([0, 0], [1, 1], g.p[1], predictor_bounds) - - # convert BBox to rect - rect = ( - Float64.([ - bbox.origin[1], - bbox.origin[1] + bbox.widths[1], - bbox.origin[2], - bbox.origin[2] + bbox.widths[2], - ])..., - ) - - b = rel_to_abs_bbox(f.scene.viewport[], rect) + bbox = calculate_BBox([0, 0], [1, 1], g.p[1], predictor_bounds, plot_radius) eeg_axis = Axis( - get_figure(f); + f, # this creates an axis at the same grid location of the current axis aspect = 1, - width = Relative(0.99), - height = Relative(0.99), - halign = b.origin[1], - valign = b.origin[2], - backgroundcolor = :white, + width = Relative(0.2), # size of bboxes + height = Relative(0.2), # size of bboxes + halign = bbox.origin[1] + bbox.widths[1] / 2, # coordinates + valign = bbox.origin[2] + bbox.widths[2] / 2, + #backgroundcolor = nothing, ) if !isnothing(labels) @@ -229,11 +219,10 @@ function plot_topo_plots!( end end -function calculate_BBox(origin, widths, predictor_value, bounds) - +function calculate_BBox(origin, widths, predictor_value, bounds, plot_radius) minwidth = minimum(widths) predictor_ratio = (predictor_value - bounds[1]) / (bounds[2] - bounds[1]) - radius = (minwidth * 0.7) / 2 + radius = (minwidth * plot_radius) / 2 # radius of the position circle of a circular topoplot size_of_BBox = minwidth / 5 # the middle point of the circle for the topoplot positions # has to be moved a bit into the direction of the longer axis @@ -251,6 +240,9 @@ function calculate_BBox(origin, widths, predictor_value, bounds) # right point of the axis. This means that you have to # move the bbox to the bottom left by size_of_bbox/2 to move # the center of the axis to a point. + if abs(y) < 1 + y = round(y, digits = 2) + end return BBox( (origin[1] + widths[1]) / 2 - size_of_BBox / 2 + x, (origin[1] + widths[1]) / 2 + size_of_BBox - size_of_BBox / 2 + x, diff --git a/src/plot_designmatrix.jl b/src/plot_designmatrix.jl index c605efd5..bea1fb62 100644 --- a/src/plot_designmatrix.jl +++ b/src/plot_designmatrix.jl @@ -48,7 +48,7 @@ function plot_designmatrix!( ) config = PlotConfig(:designmat) config_kwargs!(config; kwargs...) - designmat = UnfoldMakie.modelmatrices(data) + designmat = UnfoldMakie.modelmatrix(data) if standardize_data designmat = designmat ./ std(designmat, dims = 1) designmat[isinf.(designmat)] .= 1.0 @@ -59,49 +59,75 @@ function plot_designmatrix!( @warn "Sorting does not make sense for time-expanded designmatrices. sort_data has been set to `false`" sort_data = false end - designmat = Matrix(designmat[end÷2-2000:end÷2+2000, :]) + designmat = Matrix(designmat[end÷2-2000:end÷2+2000, :]) # needs a size(designmat) of at least 4000 x Any end if sort_data designmat = Base.sortslices(designmat, dims = 1) end - labels = Unfold.get_coefnames(data) - - lLength = length(labels) + labels0 = replace.(Unfold.get_coefnames(data), r"\s*:" => ":") + + if length(split(labels0[1], ": ")) > 1 + labels = map(x -> join(split(x, ": ")[3]), labels0) + labels_top1 = Unfold.extract_coef_info(Unfold.get_coefnames(data), 2) + unique_names = String[] + labels_top2 = String[""] + for el in labels_top1 + if !in(el, unique_names) + push!(unique_names, el) + push!(labels_top2, el) + else + push!(labels_top2, "") + end + end + end + lLength = length(labels0) # only change xticks if we want less then all if (xticks !== nothing && xticks < lLength) @assert(xticks >= 0, "xticks shouldn't be negative") # sections between xticks - sectionSize = (lLength - 2) / (xticks - 1) - newLabels = [] + section_size = (lLength - 2) / (xticks - 1) + new_labels = [] # first tick. Empty if 0 ticks if xticks >= 1 - push!(newLabels, labels[1]) + push!(new_labels, labels0[1]) else - push!(newLabels, "") + push!(new_labels, "") end # fill in ticks in the middle for i = 1:(lLength-2) # checks if we're at the end of a section, but NO tick on the very last section - if i % sectionSize < 1 && i < ((xticks - 1) * sectionSize) - push!(newLabels, labels[i+1]) + if i % section_size < 1 && i < ((xticks - 1) * section_size) + push!(new_labels, labels0[i+1]) else - push!(newLabels, "") + push!(new_labels, "") end end # last tick at the end if xticks >= 2 - push!(newLabels, labels[lLength-1]) + push!(new_labels, labels0[lLength-1]) else - push!(newLabels, "") + push!(new_labels, "") end - labels = newLabels + labels0 = new_labels + end + if length(split(labels0[1], ": ")) > 1 + ax2 = Axis( + f[1, 1], + xticklabelcolor = :red, + xaxisposition = :top; + xticks = (1:length(labels_top2), labels_top2), + ) + hidespines!(ax2) + hidexdecorations!(ax2, ticklabels = false, ticks = false) + hm = heatmap!(ax2, designmat'; config.visual...) + else + labels = labels0 end - # plot Designmatrix config.axis = merge(config.axis, (; xticks = (1:length(labels), labels))) @@ -112,7 +138,13 @@ function plot_designmatrix!( ax.yreversed = true end + + + apply_layout_settings!(config; fig = f, hm = hm) return f end +# Unfold.extract_coef_info.(Unfold.get_coefnames.(designmatrix(td)),3) +# use it! +# vcat(Unfold.extract_coef_info.(Unfold.get_coefnames.(designmatrix(td)),3)...) diff --git a/src/plot_erp.jl b/src/plot_erp.jl index 62a5b334..573e0b30 100644 --- a/src/plot_erp.jl +++ b/src/plot_erp.jl @@ -56,7 +56,7 @@ Plot a Butterfly plot. Change the size of the electrode markers in topoplot. - `topowidth::Real = 0.25` \\ Change the width of inlay topoplot. -- `topoheigth::Real = 0.25` \\ +- `topoheight::Real = 0.25` \\ Change the height of inlay topoplot. - `topopositions_to_color::x -> pos_to_color_RomaO(x)`\\ Change the line colors. @@ -80,7 +80,10 @@ plot_butterfly!( topolegend = true, topomarkersize = 10, topowidth = 0.25, - topoheigth = 0.25, + topoheight = 0.25, + topohalign = 0.05, + topovalign = 0.95, + topoaspect = 1, topopositions_to_color = x -> pos_to_color_RomaO(x), kwargs..., ) @@ -98,8 +101,11 @@ function plot_erp!( topolegend = nothing, topomarkersize = nothing, topowidth = nothing, - topoheigth = nothing, + topoheight = nothing, topopositions_to_color = nothing, + topohalign = 0.05, + topovalign = 0.95, + topoaspect = 1, mapping = (;), kwargs..., ) @@ -140,13 +146,13 @@ function plot_erp!( topolegend = false colors = nothing else - allPositions = get_topo_positions(; positions = positions, labels = labels) + all_positions = get_topo_positions(; positions = positions, labels = labels) if (config.visual.colormap !== nothing) colors = config.visual.colormap un = length(unique(plot_data[:, config.mapping.color])) colors = cgrad(config.visual.colormap, un, categorical = true) else - colors = get_topo_color(allPositions, topopositions_to_color) + colors = get_topo_color(all_positions, topopositions_to_color) end end end @@ -183,7 +189,7 @@ function plot_erp!( end # remove x / y - mappingOthers = deleteKeys(config.mapping, [:x, :y]) + mappingOthers = deleteKeys(config.mapping, [:x, :y, :positions, :lables]) xy_mapp = AlgebraOfGraphics.mapping(config.mapping.x, config.mapping.y; mappingOthers...) @@ -202,21 +208,20 @@ function plot_erp!( basic = basic + addPvalues(plot_data, pvalue, config) end - plotEquation = basic * mapp + plot_equation = basic * mapp f_grid = f[1, 1] # butterfly plot is drawn slightly different if butterfly - # no extra legend # add topolegend if (topolegend) topoAxis = Axis( f_grid, width = Relative(topowidth), - height = Relative(topoheigth), - halign = 0.05, - valign = 0.95, - aspect = 1, + height = Relative(topoheight), + halign = topohalign, + valign = topovalign, + aspect = topoaspect, ) ix = unique(i -> plot_data[:, config.mapping.group[1]][i], 1:size(plot_data, 1)) topoplotLegend( @@ -224,22 +229,22 @@ function plot_erp!( topomarkersize, plot_data[ix, config.mapping.color[1]], colors, - allPositions, + all_positions, ) end if isnothing(colors) - drawing = draw!(f_grid, plotEquation; axis = config.axis) + drawing = draw!(f_grid, plot_equation; axis = config.axis) else drawing = draw!( f_grid, - plotEquation; + plot_equation; axis = config.axis, palettes = (color = colors,), ) end else # draw a normal ERP lineplot - drawing = draw!(f_grid, plotEquation; axis = config.axis) + drawing = draw!(f_grid, plot_equation; axis = config.axis) end apply_layout_settings!(config; fig = f, ax = drawing, drawing = drawing) return f @@ -258,10 +263,10 @@ function eegHeadMatrix(positions, center, radius) end # topopositions_to_color = colors? -function topoplotLegend(axis, topomarkersize, unique_val, colors, allPositions) - allPositions = unique(allPositions) +function topoplotLegend(axis, topomarkersize, unique_val, colors, all_positions) + all_positions = unique(all_positions) - topoMatrix = eegHeadMatrix(allPositions, (0.5, 0.5), 0.5) + topoMatrix = eegHeadMatrix(all_positions, (0.5, 0.5), 0.5) un = unique(unique_val) specialColors = ColorScheme( @@ -272,11 +277,11 @@ function topoplotLegend(axis, topomarkersize, unique_val, colors, allPositions) ylims!(low = -0.2, high = 1.2) topoplot = eeg_topoplot!( axis, - 1:length(allPositions), # go from 1:npos - string.(1:length(allPositions)); - positions = allPositions, + 1:length(all_positions), # go from 1:npos + string.(1:length(all_positions)); + positions = all_positions, interpolation = NullInterpolator(), # inteprolator that returns only 0, which is put to white in the specialColorsmap - colorrange = (0, length(allPositions)), # add the 0 for the white-first color + colorrange = (0, length(all_positions)), # add the 0 for the white-first color colormap = specialColors, head = (color = :black, linewidth = 1, model = topoMatrix), label_scatter = (markersize = topomarkersize, strokewidth = 0.5), diff --git a/src/plot_erpimage.jl b/src/plot_erpimage.jl index 7d6af6ef..f0637aef 100644 --- a/src/plot_erpimage.jl +++ b/src/plot_erpimage.jl @@ -125,7 +125,6 @@ function plot_erpimage!( xlabel = "Time [s]", xlabelpadding = 0, xautolimitmargin = (0, 0), - #xticks = @lift([round(minimum($sortvalues), digits=2), round(mean($sortvalues), digits=2), round(maximum($sortvalues), digits=2)]), limits = @lift(( minimum($times), maximum($times), diff --git a/src/plot_parallelcoordinates.jl b/src/plot_parallelcoordinates.jl index 614de497..e5323f2b 100644 --- a/src/plot_parallelcoordinates.jl +++ b/src/plot_parallelcoordinates.jl @@ -185,7 +185,7 @@ function parallelcoordinates( color_ix = [findfirst(un_c .== c) for c in color] #@assert length(un_c) == 1 "Only single color found, please don't specify color, " if length(un_c) == 1 - @warn "only a single unique value found in specified color-vec" + @warn "The only a single unique value found in the specified color vector" color = cgrad(colormap, 2)[color_ix] else color = cgrad(colormap, length(un_c))[color_ix] @@ -259,20 +259,19 @@ function parallelcoordinates( ax_pcp = Makie.LineAxis( scene; limits = limits, + dim_convert = Makie.NoDimConversion(), ticks = PCPTicks(), endpoints = axis_endpoints, tickformat = tickformater, axesOptions..., ) + pcp_title!( scene, ax_pcp.attributes.endpoints, ax_labels[i]; titlegap = def[:titlegap], ) - - - append!(axlist, [ax_pcp]) end diff --git a/src/plot_topoplotseries.jl b/src/plot_topoplotseries.jl index d1c8342c..612b3102 100644 --- a/src/plot_topoplotseries.jl +++ b/src/plot_topoplotseries.jl @@ -1,6 +1,6 @@ """ - plot_topoplotseries(f::Union{GridPosition, GridLayout, Figure}, data::DataFrame, Δbin::Real; kwargs...) - plot_topoplotseries!(data::DataFrame, Δbin::Real; kwargs...) + plot_topoplotseries(f::Union{GridPosition, GridLayout, Figure}, data::DataFrame; kwargs...) + plot_topoplotseries!(data::DataFrame; kwargs...) Multiple miniature topoplots in regular distances. ## Arguments @@ -8,22 +8,25 @@ Multiple miniature topoplots in regular distances. - `f::Union{GridPosition, GridLayout, GridLayoutBase.GridSubposition, Figure}`\\ `Figure`, `GridLayout`, `GridPosition`, or GridLayoutBase.GridSubposition to draw the plot. - `data::Union{<:Observable{<:DataFrame},DataFrame}`\\ - DataFrame with data or Observable DataFrame. Requires a `time` column. -- `Δbin::Real`\\ - A number for how large one time bin should be.\\ - `Δbin` is in units of the `data.time` column.\\ - Should be `0` if `mapping.col` or `mapping.row` are categorical. + DataFrame with data or Observable DataFrame. Requires a `time` column, but could be also specified in mapping.x. ## Keyword arguments (kwargs) +- `bin_width::Real = nothing`\\ + Number specifing the width of time bin.\\ + `bin_width` is in units of the `data.time` column.\\ +- `bin_num::Real = nothing`\\ + Number of topoplots.\\ + Either `bin_width`, or `bin_num` should be specified. Error if they are both specified\\ + If `mapping.col` or `mapping.row` are categorical `bin_width` and `bin_num` should be `nothing`. - `combinefun::Function = mean`\\ - Specify how the samples within `Δbin` are summarised.\\ + Specify how the samples within `bin_width` are summarised.\\ Example functions: `mean`, `median`, `std`. - `rasterize_heatmaps::Bool = true`\\ Force rasterization of the plot heatmap when saving in `svg` format.\\ Except for the interpolated heatmap, all lines/points are vectors.\\ This is typically what you want, otherwise you get ~128x128 vectors per topoplot, which makes everything super slow. - `col_labels::Bool`, `row_labels::Bool = true`\\ - Shows column and row labels. + Shows column and row labels for categorical values. - `labels::Vector{String} = nothing`\\ Show labels for each electrode. - `positions::Vector{Point{2, Float32}} = nothing`\\ @@ -32,22 +35,31 @@ Multiple miniature topoplots in regular distances. Enable interactive mode. \\ If you create `obs_tuple = Observable((0, 0, 0))` and pass it into `interactive_scatter` you can change observable indecies by clicking topopplot markers.\\ `(0, 0, 0)` corresponds to the indecies of row of topoplot layout, column of topoplot layout and channell. +- `mapping.x = :time`\\ + Specification of x value, could be any contionous variable. +- `mapping.layout = nothing`\\ + When equals `:time` arrange topoplots by rows. + $(_docstring(:topoplotseries)) **Return Value:** `Figure` displaying the Topoplot series. """ -plot_topoplotseries(data::DataFrame, Δbin::Real; kwargs...) = - plot_topoplotseries!(Figure(), data, Δbin; kwargs...) +plot_topoplotseries(data::DataFrame; kwargs...) = + plot_topoplotseries!(Figure(), data; kwargs...) + +#@deprecate plot_topoplotseries(data::DataFrame, Δbin; kwargs...) plot_topoplotseries(data::DataFrame; bin_width, kwargs...) function plot_topoplotseries!( f::Union{GridPosition,GridLayout,Figure,GridLayoutBase.GridSubposition}, - data::Union{<:Observable{<:DataFrame},DataFrame}, - Δbin; + data::Union{<:Observable{<:DataFrame},DataFrame}; + bin_width = nothing, + bin_num = nothing, positions = nothing, - labels = nothing, + nrows = 1, + labels = nothing, # rename to channel_labels? combinefun = mean, - col_labels = true, + col_labels = false, row_labels = true, rasterize_heatmaps = true, interactive_scatter = nothing, @@ -55,6 +67,8 @@ function plot_topoplotseries!( ) data = _as_observable(data) + positions = get_topo_positions(; positions = positions, labels = labels) + chan_or_label = "label" ∉ names(to_value(data)) ? :channel : :label config = PlotConfig(:topoplotseries) # overwrite all defaults by user specified values @@ -62,24 +76,51 @@ function plot_topoplotseries!( # resolve columns with data config.mapping = resolve_mappings(to_value(data), config.mapping) - cat_or_cont_columns = eltype(to_value(data)[!, config.mapping.col]) <: Number ? "cont" : "cat" + data = (to_value(data)) if cat_or_cont_columns == "cat" # overwrite Time windows [s] default if categorical config_kwargs!(config; axis = (; xlabel = string(config.mapping.col))) config_kwargs!(config; kwargs...) # add the user specified once more, just if someone specifies the xlabel manually - # overkll as we would only need to check the xlabel ;) - end - - positions = get_topo_positions(; positions = positions, labels = labels) + # overkll as we would only need to check the xlabel ;) + else + # arrangment of topoplots by rows and cols + bins = bins_estimation(data.time; bin_width, bin_num, cat_or_cont_columns) + n_topoplots = number_of_topoplots(data; bin_width, bin_num, bins, config.mapping) - chan_or_label = "label" ∉ names(to_value(data)) ? :channel : :label + data.timecuts = cut(data.time, bins; extend = true) + unique_cuts = unique(data.timecuts) + ix = findall.(isequal.(unique_cuts), [data.timecuts]) + if :layout ∈ keys(config.mapping) + n_cols = Int(ceil(sqrt(n_topoplots))) + n_rows = Int(ceil(n_topoplots / n_cols)) + else + n_rows = nrows + if 0 > n_topoplots / nrows + @warn "Impossible number of rows, set to 1 row" + n_rows = 1 + elseif n_topoplots / nrows < 1 + @warn "Impossible number of rows, set to $(n_topoplots) rows" + end + n_cols = Int(ceil(n_topoplots / n_rows)) + end + _col = repeat(1:n_cols, outer = n_rows)[1:n_topoplots] + _row = repeat(1:n_rows, inner = n_cols)[1:n_topoplots] + data._col .= 1 + data._row .= 1 + for topo = 1:n_topoplots + data._col[ix[topo]] .= _col[topo] + data._row[ix[topo]] .= _row[topo] + end + config_kwargs!(config; mapping = (; row = :_row, col = :_col)) + end ftopo, axlist = eeg_topoplot_series!( f, - data, - Δbin; + data; + bin_width = bin_width, + bin_num = bin_num, y = config.mapping.y, label = chan_or_label, col = config.mapping.col, @@ -99,8 +140,9 @@ function plot_topoplotseries!( else data_mean = if cat_or_cont_columns == "cont" df_timebin( - to_value(data), - Δbin; + to_value(data); + bin_width, + bin_num, col_y = config.mapping.y, fun = combinefun, grouping = [chan_or_label, config.mapping.col, config.mapping.row], @@ -119,24 +161,92 @@ function plot_topoplotseries!( if !config.layout.use_colorbar config_kwargs!(config, layout = (; use_colorbar = false, show_legend = false)) end - ax = Axis( - f[1, 1], - xlabel = config.axis.xlabel, - ylabel = config.axis.ylabel, - title = config.axis.title, - titlesize = config.axis.titlesize, - titlefont = config.axis.titlefont, - ylabelpadding = config.axis.ylabelpadding, - xlabelpadding = config.axis.xlabelpadding, - xpanlock = config.axis.xpanlock, - ypanlock = config.axis.ypanlock, - xzoomlock = config.axis.xzoomlock, - yzoomlock = config.axis.yzoomlock, - xrectzoom = config.axis.xrectzoom, - yrectzoom = config.axis.yrectzoom, + f[1, 1]; + (p for p in pairs(config.axis) if p[1] != :xlim_topo && p[1] != :ylim_topo)..., ) apply_layout_settings!(config; fig = f, ax = ax) return f +end + +function bins_estimation( + time; + bin_width = nothing, + bin_num = nothing, + cat_or_cont_columns = "cont", +) + tmin = minimum(time) + tmax = maximum(time) + if (!isnothing(bin_width) && !isnothing(bin_num)) + error("Ambigious parameters: specify only `bin_width` or `bin_num`.") + elseif (isnothing(bin_width) && isnothing(bin_num) && cat_or_cont_columns == "cont") + error( + "You haven't specified `bin_width` or `bin_num`. Such option is available only with categorical `mapping.col` or `mapping.row`.", + ) + end + if isnothing(bin_width) + bins = range(; start = tmin, length = bin_num + 1, stop = tmax) + else + bins = range(; start = tmin, step = bin_width, stop = tmax) + end + return bins +end + +function number_of_topoplots( + df::DataFrame; + bin_width = nothing, + bin_num = nothing, + bins, + mapping = config.mapping, +) + if !isnothing(bin_width) + time_new = cut(df.time, bins; extend = true) + n = length(unique(time_new)) + elseif !isnothing(bin_num) + time_new = cut(df.time, bins; extend = true) + n = length(unique(time_new)) + else + n = unique(df[:, mapping.col]) + end + return n +end + + +""" + df_timebin(df, bin_width; col_y = :erp, fun = mean, grouping = []) +Split or combine `DataFrame` according to equally spaced time bins. + +Arguments: +- `df::AbstractTable`\\ + With columns `:time` and `col_y` (default `:erp`), and all columns in `grouping`; +- `bin_width::Real = nothing`\\ + Bin width in `:time` units; +- `bin_num::Real = nothing`\\ + Number of topoplots; +- `col_y = :erp` \\ + The column to combine over (with `fun`); +- `fun = mean()`\\ + Function to combine. +- `grouping = []`\\ + Vector of symbols or strings, columns to group the data by before aggregation. Values of `nothing` are ignored. + +**Return Value:** `DataFrame`. +""" +function df_timebin( + df; + bin_width = nothing, + bin_num = nothing, + col_y = :erp, + fun = mean, + grouping = [], +) + bins = bins_estimation(df.time; bin_width, bin_num, cat_or_cont_columns = "cont") + df = deepcopy(df) # cut seems to change stuff inplace + df.time = cut(df.time, bins; extend = true) + + grouping = grouping[.!isnothing.(grouping)] + df_m = combine(groupby(df, unique([:time, grouping...])), col_y => fun) + rename!(df_m, names(df_m)[end] => col_y) # remove the fun part of the new column + return df_m end diff --git a/src/plotconfig.jl b/src/plotconfig.jl index 52c913dc..aaefc906 100644 --- a/src/plotconfig.jl +++ b/src/plotconfig.jl @@ -105,14 +105,6 @@ function PlotConfig(T::Val{:circtopos}) return cfg end - -function PlotConfig(T::Val{:topoarray}) - cfg = PlotConfig(:erp) - - config_kwargs!(cfg; layout = (;), colorbar = (;), mapping = (;), axis = (;)) - return cfg -end - function PlotConfig(T::Val{:topoplot}) cfg = PlotConfig() @@ -120,8 +112,6 @@ function PlotConfig(T::Val{:topoplot}) cfg; layout = ( show_legend = true, - xlabelFromMapping = nothing, - ylabelFromMapping = nothing, use_colorbar = true, hidespines = (), hidedecorations = (Dict(:label => false)), diff --git a/test/runtests.jl b/test/runtests.jl index 41566155..9d9ed5c1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,8 +26,12 @@ end include("test_topoplot.jl") end -@testset "Topoplot series" begin - include("test_toposeries.jl") +@testset "Topoplot series simple" begin + include("test_toposeries1.jl") +end + +@testset "Topoplot series advanced" begin + include("test_toposeries2.jl") end @testset "Parallel coordinates plot" begin diff --git a/test/setup.jl b/test/setup.jl index bee46077..821377de 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -1,6 +1,6 @@ +using CairoMakie using UnfoldSim using Test -using CairoMakie using GeometryBasics using DataFrames using TopoPlots diff --git a/test/test_butterfly.jl b/test/test_butterfly.jl index b59ef198..c4734e6d 100644 --- a/test/test_butterfly.jl +++ b/test/test_butterfly.jl @@ -2,21 +2,32 @@ include("../docs/example_data.jl") df, pos = example_data("TopoPlots.jl") -@testset "butterfly basic" begin - plot_butterfly(df; positions = pos) +@testset "butterfly default" begin + plot_butterfly(df; positions = pos, visual = (; transparency = true)) + #save("dev/UnfoldMakie/default_butterfly.png", f) end -@testset "butterfly basic with GridLayout" begin +@testset "butterfly default with GridLayout" begin f = Figure() plot_butterfly!(f[1, 1], df; positions = pos) end +@testset "butterfly basic" begin + plot_butterfly( + df; + positions = pos, + topopositions_to_color = x -> Colors.RGB(0.1), + topolegend = false, + ) + #save("dev/UnfoldMakie/basic_butterfly.png", f) +end + @testset "butterfly with change of topomarkersize" begin plot_butterfly( df; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, ) end @@ -47,7 +58,7 @@ end df; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, layout = (; hidedecorations = (:label => true, :ticks => true, :ticklabels => true) @@ -151,9 +162,8 @@ end end #TO DO - # not working -@testset "butterfly with two size highlighted channels" begin +#= @testset "butterfly with two size highlighted channels" begin df.highlight = in.(df.channel, Ref([10, 12])) plot_butterfly(df; positions = pos, mapping = (; linesize = :highlight)) -end +end =# diff --git a/test/test_circular_topoplots.jl b/test/test_circular_topoplots.jl index 8856a3b2..26f6e1f4 100644 --- a/test/test_circular_topoplots.jl +++ b/test/test_circular_topoplots.jl @@ -56,12 +56,12 @@ end end @testset "testing calculate_BBox" begin - @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], 180, [0, 360]) == - BBox(50.0, 250.0, 400.0, 600.0) - @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], -45, [0, 360]) == - BBox(647.48737, 847.48737, 152.51262, 352.51262) - @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], -180, [-180, 180]) == - BBox(750.0, 950.0, 400.0, 600.0) + @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], 180, [0, 360], 0.8) == + BBox(0.0, 200.0, 400.0, 600) + @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], -45, [0, 360], 0.8) == + BBox(682.842712474619, 882.842712474619, 117.15728752538104, 317.15728752538104) + @test UnfoldMakie.calculate_BBox([0, 0], [1000, 1000], -180, [-180, 180], 0.8) == + BBox(800.0, 1000.0, 400.0, 600.0) end @testset "circularplot plot basic" begin diff --git a/test/test_complexplots.jl b/test/test_complexplots.jl index bedb17ed..cb74ad4d 100644 --- a/test/test_complexplots.jl +++ b/test/test_complexplots.jl @@ -34,7 +34,7 @@ d_topo; positions = pos, topomarkersize = 10, - topoheigth = 0.4, + topoheight = 0.4, topowidth = 0.4, ) hlines!(0, color = :gray, linewidth = 1) @@ -48,8 +48,8 @@ plot_topoplotseries!( gd, - df, - 80; + df; + bin_width = 80, positions = positions, visual = (label_scatter = false,), layout = (; use_colorbar = true), @@ -130,8 +130,8 @@ end plot_topoplot!(f[2, 1], data[:, 150, 1]; positions = positions) plot_topoplotseries!( f[2, 2], - d_topo, - 0.1; + d_topo; + bin_width = 0.1, positions = positions, visual = (label_scatter = false,), layout = (; use_colorbar = true), @@ -200,8 +200,8 @@ end plot_topoplot!(f[3, 1], data[:, 150, 1]; positions = positions) plot_topoplotseries!( f[4, 1:3], - d_topo, - 0.1; + d_topo; + bin_width = 0.1, positions = positions, mapping = (; label = :channel), ) diff --git a/test/test_dm.jl b/test/test_dm.jl index 867022a7..5fe401e2 100644 --- a/test/test_dm.jl +++ b/test/test_dm.jl @@ -24,9 +24,13 @@ end @testset "hierarchical labels (bugged)" begin df, evts = UnfoldSim.predef_eeg() f = @formula 0 ~ 1 + condition + continuous - #basisfunction = firbasis(τ = (-0.4, 0.8), sfreq = 100, name = "stimulus") - basisfunction = firbasis(τ = (-0.4, -0.3), sfreq = 10, name = "") + basisfunction = firbasis(τ = (-0.4, 0.8), sfreq = 5, name = "stimulus") + #basisfunction = firbasis(τ = (-0.4, -0.3), sfreq = 10, name = "") bfDict = [Any => (f, basisfunction)] td = fit(UnfoldModel, bfDict, evts, df) plot_designmatrix(designmatrix(td)) end + + +#Unfold.SimpleTraits.istrait(Unfold.ContinuousTimeTrait{typeof(td)}) +#Unfold.SimpleTraits.istrait(Unfold.ContinuousTimeTrait{typeof(uf)}) diff --git a/test/test_erpimage.jl b/test/test_erpimage.jl index 81008003..bfb914f7 100644 --- a/test/test_erpimage.jl +++ b/test/test_erpimage.jl @@ -125,12 +125,13 @@ end sortval = Observable(evts_e.continuous) end +@testset "check error of empty sortvalues" begin + err1 = nothing + t() = error(plot_erpimage(times, dat_e; show_sortval = true)) + try + t() + catch err1 + end - -#= @testset "ERP image with show_sortval" begin - plot_erpimage( - times, - dat_e; - show_sortval = true, - ) #should learn that it must be Error -=# + @test err1 == ErrorException("`show_sortval` needs non-empty `sortvalues` argument") +end diff --git a/test/test_toposeries.jl b/test/test_toposeries.jl deleted file mode 100644 index 4bc06270..00000000 --- a/test/test_toposeries.jl +++ /dev/null @@ -1,280 +0,0 @@ -dat, positions = TopoPlots.example_data() -df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) -Δbin = 80 - -@testset "toposeries basic" begin - plot_topoplotseries(df, Δbin; positions = positions) -end - -@testset "toposeries basic with channel names" begin - plot_topoplotseries(df, Δbin; positions = positions, labels = raw_ch_names) -end - -@testset "toposeries with xlabel" begin - f = Figure() - ax = Axis(f[1, 1]) - plot_topoplotseries!(f[1, 1], df, Δbin; positions = positions) - text!(ax, 0, 0, text = "Time [ms] ", align = (:center, :center), offset = (0, -120)) - hidespines!(ax) # delete unnecessary spines (lines) - hidedecorations!(ax, label = false) - f -end - -@testset "toposeries for one time point" begin - plot_topoplotseries(df, Δbin; positions = positions, combinefun = x -> x[end÷2]) -end - -@testset "toposeries with differend comb functions " begin - f = Figure(size = (500, 500)) - plot_topoplotseries!( - f[1, 1], - df, - Δbin; - positions = positions, - combinefun = mean, - axis = (; title = "combinefun = mean"), - ) - plot_topoplotseries!( - f[2, 1], - df, - Δbin; - positions = positions, - combinefun = median, - axis = (; title = "combinefun = median"), - ) - plot_topoplotseries!( - f[3, 1], - df, - Δbin; - positions = positions, - combinefun = std, - axis = (; title = "combinefun = std"), - ) - f -end - -@testset "toposeries without colorbar" begin - plot_topoplotseries(df, Δbin; positions = positions, layout = (; use_colorbar = false)) -end - -@testset "GridPosition with a title" begin - f = Figure() - ax = Axis(f[1:2, 1:5], aspect = DataAspect(), title = "Just a title") - - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) - - Δbin = 80 - a = plot_topoplotseries!( - f[1:2, 1:5], - df, - Δbin; - positions = positions, - layout = (; use_colorbar = true), - ) - hidespines!(ax) - hidedecorations!(ax, label = false) - - f -end - - -@testset "14 topoplots and GridPosition" begin # horrific - f = Figure() - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) - Δbin = 30 - plot_topoplotseries!( - f[1, 1:5], - df, - Δbin; - positions = positions, - visual = (label_scatter = false,), - ) - f -end - - -@testset "row faceting, 2 conditions" begin - f = Figure() - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) - df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) - - plot_topoplotseries!( - f[1:2, 1:2], - df, - Δbin; - col_labels = true, - mapping = (; row = :condition), - positions = positions, - visual = (label_scatter = false,), - layout = (; use_colorbar = true), - ) - f -end - -@testset "row faceting, 5 conditions" begin - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) - df.condition = repeat(["A", "B", "C", "D", "E"], size(df, 1) ÷ 5) - - f = Figure(size = (600, 500)) - plot_topoplotseries!( - f[1:2, 1:2], - df, - Δbin; - col_labels = true, - mapping = (; row = :condition), - axis = (; ylabel = "Conditions"), - positions = positions, - visual = (label_scatter = false,), - layout = (; use_colorbar = true), - ) - f -end - -@testset "toposeries with xlabel" begin - plot_topoplotseries(df, Δbin; positions = positions, axis = (; xlabel = "test")) -end -@testset "toposeries with adjustable colorrange" begin - plot_topoplotseries( - df, - Δbin; - positions = positions, - colorbar = (; colorrange = (-1, 1)), - ) -end -@testset "toposeries with xlabel" begin - plot_topoplotseries(df, Δbin; positions = positions, axis = (; ylim_topo = (0, 0.7))) -end - -@testset "basic eeg_topoplot_series" begin - df = DataFrame( - :erp => repeat(1:63, 100), - :time => repeat(1:20, 5 * 63), - :label => repeat(1:63, 100), - ) # simulated data - a = (sin.(range(-2 * pi, 2 * pi, 63))) - b = [(1:63) ./ 63 .* a (1:63) ./ 63 .* cos.(range(-2 * pi, 2 * pi, 63))] - pos = b .* 0.5 .+ 0.5 # simulated electrode positions - pos = [Point2.(pos[k, 1], pos[k, 2]) for k = 1:size(pos, 1)] - UnfoldMakie.eeg_topoplot_series(df, 5; positions = pos) -end - -@testset "toposeries with GridSubposition" begin - f = Figure(size = (500, 500)) - plot_topoplotseries!( - f[2, 1][1, 1], - df, - Δbin; - positions = positions, - combinefun = mean, - axis = (; title = "combinefun = mean"), - ) -end - - -@testset "observables" begin - f = Figure() - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) - df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) - - df_obs = Observable(df) - - plot_topoplotseries!( - f[1:2, 1:2], - df_obs, - Δbin; - col_labels = true, - mapping = (; row = :condition), - positions = positions, - visual = (label_scatter = false,), - layout = (; use_colorbar = true), - ) - f - df = to_value(df_obs) - df.estimate .= rand(length(df.estimate)) - df_obs[] = df -end - -@testset "categorical cols" begin - f = Figure() - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) - df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) - - plot_topoplotseries!( - f[1:2, 1:2], - df, - 0; - col_labels = true, - mapping = (; col = :condition), - positions = positions, - visual = (label_scatter = false,), - layout = (; use_colorbar = true), - ) - f - -end - -@testset "observables on scatterpoints" begin - ##-- - df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) - df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) - - obs_tuple = Observable((0, 0, 0)) - plot_topoplotseries( - df, - 0; - col_labels = true, - mapping = (; col = :condition), - positions = positions, - visual = (label_scatter = (markersize = 15, strokewidth = 2),), - layout = (; use_colorbar = true), - interactive_scatter = obs_tuple, - ) - - -end - - -#= -using CSV -tmp = CSV.read("dev/UnfoldMakie/test/output2.csv", DataFrame) -tmp = stack(tmp, 1:21) -rename!(tmp, :variable => :condition, :value => :estimate) -tmp.time = 1:nrow(tmp) - -Δbin = 140 - -tmp2 = filter(x -> x.condition == "type" || x.condition == "duration", tmp) - -plot_topoplotseries( - tmp2, - Δbin; - positions = rand(Point2f, 128), - combinefun = x -> x, - mapping = (; :col => :condition), -) - - -tmp3 = filter(x -> x.condition != "type", tmp) -n = size(tmp3, 1) ÷ 4 -tmp3.row = vcat(fill("A", n), fill("B", n), fill("C", n), fill("D", n)) - -Δbin = 134 -plot_topoplotseries( - tmp3, - Δbin; - positions = rand(Point2f, 128), - combinefun = x -> x, - mapping = (; :col => :condition, :row => :row), -) - -using Images - -function filt(img) - filtered_data = UnfoldMakie.imfilter(img, UnfoldMakie.Kernel.gaussian((1, max(30, 0)))) -end - -Images.entropy((dat[:, :, shuffle(1:end)])) - - -map((x) -> Images.entropy((dat[x, :, shuffle(1:end)])) * 2, 1:size(dat, 1)) - =# diff --git a/test/test_toposeries1.jl b/test/test_toposeries1.jl new file mode 100644 index 00000000..86e802f0 --- /dev/null +++ b/test/test_toposeries1.jl @@ -0,0 +1,179 @@ +# simple checks + +dat, positions = TopoPlots.example_data() +df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) +bin_width = 80 + +@testset "toposeries basic with bin_width" begin + plot_topoplotseries(df; bin_width, positions = positions) +end + +@testset "toposeries basic with bin_num" begin + plot_topoplotseries(df; bin_num = 5, positions = positions) +end + +@testset "toposeries basic: checking mapping" begin + plot_topoplotseries(df; bin_num = 5, positions = positions, mapping = (; x = df.time)) +end + +#= @testset "toposeries with Δbin deprecated" begin #fail + plot_topoplotseries(df, Δbin; positions = positions) +end =# + +@testset "toposeries basic with nrows specified" begin + plot_topoplotseries(df; bin_num = 5, nrows = 2, positions = positions) +end + +@testset "toposeries basic with nrows specified" begin + plot_topoplotseries(df; bin_num = 5, nrows = 3, positions = positions) +end + +@testset "toposeries basic with nrows specified" begin + plot_topoplotseries(df; bin_num = 5, nrows = -6, positions = positions) +end + +@testset "error checking: bin_width and bin_num specified" begin + err1 = nothing + t() = error(plot_topoplotseries(df; bin_width, bin_num = 5, positions = positions)) + try + t() + catch err1 + end + @test err1 == + ErrorException("Ambigious parameters: specify only `bin_width` or `bin_num`.") +end + +@testset "error checking: bin_width and bin_num not specified" begin + err1 = nothing + t() = error(plot_topoplotseries(df; positions = positions)) + try + t() + catch err1 + end + @test err1 == ErrorException( + "You haven't specified `bin_width` or `bin_num`. Such option is available only with categorical `mapping.col` or `mapping.row`.", + ) +end + +@testset "toposeries basic with channel names" begin + plot_topoplotseries(df; bin_width, positions = positions, labels = raw_ch_names) +end # doesnt work rn + +@testset "toposeries with xlabel" begin + f = Figure() + ax = Axis(f[1, 1]) + plot_topoplotseries!(f[1, 1], df; bin_width, positions = positions) + text!(ax, 0, 0, text = "Time [ms] ", align = (:center, :center), offset = (0, -120)) + hidespines!(ax) # delete unnecessary spines (lines) + hidedecorations!(ax, label = false) + f +end + +@testset "toposeries for one time point (?)" begin + plot_topoplotseries(df; bin_width, positions = positions, combinefun = x -> x[end÷2]) +end + +@testset "toposeries with differend comb functions " begin + f = Figure(size = (500, 500)) + plot_topoplotseries!( + f[1, 1], + df; + bin_width, + positions = positions, + combinefun = mean, + axis = (; xlabel = "", title = "combinefun = mean"), + ) + plot_topoplotseries!( + f[2, 1], + df; + bin_width, + positions = positions, + combinefun = median, + axis = (; xlabel = "", title = "combinefun = median"), + ) + plot_topoplotseries!( + f[3, 1], + df; + bin_width, + positions = positions, + combinefun = std, + axis = (; title = "combinefun = std"), + ) + f +end + +@testset "toposeries without colorbar" begin + plot_topoplotseries( + df; + bin_width, + positions = positions, + layout = (; use_colorbar = false), + ) +end + +@testset "GridPosition with a title" begin + f = Figure() + ax = Axis(f[1:2, 1:5], aspect = DataAspect(), title = "Just a title") + + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) + + bin_width = 80 + a = plot_topoplotseries!( + f[1:2, 1:5], + df; + bin_width, + positions = positions, + layout = (; use_colorbar = true), + ) + hidespines!(ax) + hidedecorations!(ax, label = false) + + f +end + +@testset "toposeries with specified xlabel" begin + plot_topoplotseries(df; bin_width, positions = positions, axis = (; xlabel = "test")) +end + +@testset "toposeries with adjustable colorrange" begin + plot_topoplotseries( + df; + bin_width, + positions = positions, + colorbar = (; colorrange = (-1, 1)), + ) +end + +@testset "toposeries with adjusted ylim_topo" begin + plot_topoplotseries( + df; + bin_width, + positions = positions, + axis = (; ylim_topo = (0, 0.7)), + ) +end + +#= @testset "basic eeg_topoplot_series" begin + df = DataFrame( + :erp => repeat(1:63, 100), + :time => repeat(1:20, 5 * 63), + :label => repeat(1:63, 100), + ) # simulated data + a = (sin.(range(-2 * pi, 2 * pi, 63))) + b = [(1:63) ./ 63 .* a (1:63) ./ 63 .* cos.(range(-2 * pi, 2 * pi, 63))] + pos = b .* 0.5 .+ 0.5 # simulated electrode positions + pos = [Point2.(pos[k, 1], pos[k, 2]) for k = 1:size(pos, 1)] + UnfoldMakie.eeg_topoplot_series(df; bin_width = 5, positions = pos) +end =# + +@testset "toposeries with GridSubposition" begin + f = Figure(size = (500, 500)) + plot_topoplotseries!( + f[2, 1][1, 1], + df; + bin_width, + positions = positions, + combinefun = mean, + axis = (; title = "combinefun = mean"), + ) +end diff --git a/test/test_toposeries2.jl b/test/test_toposeries2.jl new file mode 100644 index 00000000..83a0acf8 --- /dev/null +++ b/test/test_toposeries2.jl @@ -0,0 +1,136 @@ +# advanced features: facetting and interactivity + +dat, positions = TopoPlots.example_data() +df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) +bin_width = 80 + +@testset "14 topoplots, 4 rows" begin # horrific + f = Figure() + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, :, 1], string.(1:length(positions))) + plot_topoplotseries!( + f[1, 1:5], + df; + bin_num = 14, + nrows = 4, + positions = positions, + visual = (; label_scatter = false), + ) + f +end + +@testset "facetting by layout" begin # could be changed to nrwos = "auto" + df = UnfoldMakie.eeg_matrix_to_dataframe( + dat[:, 200:1:206, 1], + string.(1:length(positions)), + ) + + f = Figure(size = (600, 500)) + plot_topoplotseries!( + f[1:2, 1:2], + df; + bin_width = 1, + mapping = (; layout = :time), + positions = positions, + ) + f +end + +@testset "categorical columns" begin + f = Figure() + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) + + plot_topoplotseries!( + f[1, 1], + df; + col_labels = true, + mapping = (; col = :condition), + positions = positions, + ) + f +end + +@testset "4 conditions" begin + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:4, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B", "C", "D"], size(df, 1) ÷ 4) + + f = Figure(size = (600, 500)) + plot_topoplotseries!( + f[1, 1], + df; + positions = positions, + col_labels = true, + axis = (; ylabel = "Conditions"), + mapping = (; col = :condition), + ) + f +end + +@testset "4 conditions in 2 rows" begin # TBD + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:4, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B", "C", "D"], size(df, 1) ÷ 4) + + f = Figure(size = (600, 500)) + plot_topoplotseries!( + f[1, 1], + df; + nrows = 2, + positions = positions, + col_labels = true, + axis = (; ylabel = "Conditions"), + mapping = (; col = :condition), + ) + f +end + +@testset "change xlabel" begin + f = Figure() + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) + + plot_topoplotseries!( + f[1, 1], + df; + col_labels = true, + mapping = (; col = :condition), + axis = (; xlabel = "test"), + positions = positions, + ) + f +end + +# use with WGlMakie +@testset "interactive data" begin + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) + + df_obs = Observable(df) + + f = Figure() + plot_topoplotseries!( + f[1, 1], + df_obs; + col_labels = true, + mapping = (; col = :condition), + positions = positions, + ) + f + df = to_value(df_obs) + df.estimate .= rand(length(df.estimate)) + df_obs[] = df +end + +@testset "interactive scatter markers" begin + df = UnfoldMakie.eeg_matrix_to_dataframe(dat[:, 1:2, 1], string.(1:length(positions))) + df.condition = repeat(["A", "B"], size(df, 1) ÷ 2) + + obs_tuple = Observable((0, 0, 0)) + plot_topoplotseries( + df; + col_labels = true, + mapping = (; col = :condition), + positions = positions, + visual = (; label_scatter = (markersize = 15, strokewidth = 2)), + interactive_scatter = obs_tuple, + ) +end