diff --git a/.gitignore b/.gitignore index 04bfe71..b26d5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ test/test_images test/Manifest.toml docs/Manifest.toml +.CondaPkg/ docs/Manifest-v*.toml deps/build.log diff --git a/Project.toml b/Project.toml index 284f29a..536bb12 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TopoPlots" uuid = "2bdbdf9c-dbd8-403f-947b-1a4e0dd41a7a" authors = ["Benedikt Ehinger", "Simon Danisch", "Beacon Biosignals, Inc."] -version = "0.1.9" +version = "0.2.0-DEV" [deps] CloughTocher2DInterpolation = "b70b374f-000b-463f-88dc-37030f004bd0" diff --git a/docs/src/eeg.md b/docs/src/eeg.md index c0997ab..d4f7946 100644 --- a/docs/src/eeg.md +++ b/docs/src/eeg.md @@ -9,12 +9,12 @@ TopoPlots.eeg_topoplot -So for the standard 10/20 montage, one can drop the `positions` attribute: +For the standard 10/20 montage, one can drop the `positions` attribute: ```@example eeg using TopoPlots, CairoMakie labels = TopoPlots.CHANNELS_10_20 -TopoPlots.eeg_topoplot(rand(19), labels; axis=(aspect=DataAspect(),), label_text=true, label_scatter=(markersize=10, strokewidth=2,)) +TopoPlots.eeg_topoplot(rand(19); labels, axis=(aspect=DataAspect(),), label_text=true, label_scatter=(markersize=10, strokewidth=2,)) ``` If the channels aren't 10/20, one can still plot them, but then the positions need to be passed as well: @@ -22,7 +22,7 @@ If the channels aren't 10/20, one can still plot them, but then the positions ne ```@example eeg data, positions = TopoPlots.example_data() labels = ["s$i" for i in 1:size(data, 1)] -TopoPlots.eeg_topoplot(data[:, 340, 1], labels; positions=positions, axis=(aspect=DataAspect(),)) +TopoPlots.eeg_topoplot(data[:, 340, 1]; labels, label_text = true, positions=positions, axis=(aspect=DataAspect(),)) ``` ```@docs diff --git a/src/eeg.jl b/src/eeg.jl index 9dd8752..f29de81 100644 --- a/src/eeg.jl +++ b/src/eeg.jl @@ -1,12 +1,13 @@ -@recipe(EEG_TopoPlot, data, labels) do scene +@recipe(EEG_TopoPlot, data) do scene return Attributes(; - head = (color=:black, linewidth=3), + head=(color=:black, linewidth=3), + labels=Makie.automatic, positions = Makie.automatic, # overwrite some topoplot defaults default_theme(scene, TopoPlot)..., - label_scatter = true, - contours = true, + label_scatter=true, + contours=true, ) end @@ -16,16 +17,22 @@ end Attributes: * `positions::Vector{<: Point} = Makie.automatic`: Can be calculated from label (channel) names. Currently, only 10/20 montage has default coordinates provided. - +* `labels::AbstractVector{<:AbstractString} = Makie.automatic`: Add custom labels, when `label_text` is set to true. If `positions` is not specified, `labels` are used to look up the 10/20 coordinates. * `head = (color=:black, linewidth=3)`: draw the outline of the head. Set to nothing to not draw the head outline, otherwise set to a namedtuple that get passed down to the `line!` call that draws the shape. # Some attributes from topoplot are set to different defaults: * `label_scatter = true` * `contours = true` Otherwise the recipe just uses the [`topoplot`](@ref) defaults and passes through the attributes. + +!!! note + You MUST set `label_text=true` for labels to display. """ eeg_topoplot +@deprecate eeg_topoplot(data::AbstractVector{<:Real}, labels::Vector{<:AbstractString}) eeg_topoplot(data; labels) +@deprecate eeg_topoplot!(fig, data::AbstractVector{<:Real}, labels::Vector{<:AbstractString}) eeg_topoplot!(fig, data; labels) + function draw_ear_nose!(parent, circle; kw...) # draw circle head_points = lift(circle) do circle @@ -33,15 +40,18 @@ function draw_ear_nose!(parent, circle; kw...) diameter = 2GeometryBasics.radius(circle) middle = GeometryBasics.origin(circle) nose = (Point2f[(-0.05, 0.5), (0.0, 0.55), (0.05, 0.5)] .* diameter) .+ (middle,) - push!(points, Point2f(NaN)); append!(points, nose) + push!(points, Point2f(NaN)) + append!(points, nose) ear = (Point2f[ (0.497, 0.0555), (0.51, 0.0775), (0.518, 0.0783), (0.5299, 0.0746), (0.5419, 0.0555), (0.54, -0.0055), (0.547, -0.0932), (0.532, -0.1313), (0.51, -0.1384), (0.489, -0.1199)] .* diameter) - push!(points, Point2f(NaN)); append!(points, ear .+ middle) - push!(points, Point2f(NaN)); append!(points, (ear .* Point2f(-1, 1)) .+ (middle,)) + push!(points, Point2f(NaN)) + append!(points, ear .+ middle) + push!(points, Point2f(NaN)) + append!(points, (ear .* Point2f(-1, 1)) .+ (middle,)) return points end @@ -57,7 +67,7 @@ const CHANNEL_TO_POSITION_10_20 = begin result = Matrix{Float64}(undef, 19, 2) read!(assetpath("layout_10_20.bin"), result) positions = Point2f.(result[:, 1], result[:, 2]) - Dict{String, Point2f}(zip(CHANNELS_10_20, positions)) + Dict{String,Point2f}(zip(CHANNELS_10_20, positions)) end """ @@ -70,26 +80,41 @@ function labels2positions(labels) if haskey(CHANNEL_TO_POSITION_10_20, key) return CHANNEL_TO_POSITION_10_20[key] else - error("Currently only 10_20 is supported. Found: $(label)") + error("Currently only 10/20 is supported. Found label: $(label)") + end end end -function Makie.convert_arguments(::Type{<:EEG_TopoPlot}, data::AbstractVector{<: Real}) - return (data, ["sensor $i" for i in 1:length(data)]) -end +#function Makie.convert_arguments(::Type{<:EEG_TopoPlot}, data::AbstractVector{<:Real}) +# return (data, labels2positions(labels))# + + # +#end function Makie.plot!(plot::EEG_TopoPlot) + positions = lift(plot.labels, plot.positions) do labels, positions + if positions isa Makie.Automatic + (!isnothing(labels) && labels != Makie.Automatic) || error("Either positions or labels (10/20-lookup) have to be specified") + return labels2positions(labels) else # apply same conversion as for e.g. the scatter arguments return convert_arguments(Makie.PointBased(), positions)[1] end end + plot.labels = lift(plot.labels, plot.positions) do labels, positions + + if isnothing(labels) || labels isa Makie.Automatic + return ["sensor $i" for i in 1:length(positions)] + else + return labels + end + end - tplot = topoplot!(plot, Attributes(plot), plot.data, positions; labels=plot.labels) + tplot = topoplot!(plot, Attributes(plot), plot.data, positions;) head = plot_or_defaults(to_value(plot.head), Attributes(), :head) if !isnothing(head) draw_ear_nose!(plot, tplot.geometry; head...) diff --git a/test/CondaPkg.toml b/test/CondaPkg.toml index b72a254..3cac7b0 100644 --- a/test/CondaPkg.toml +++ b/test/CondaPkg.toml @@ -2,7 +2,7 @@ channels = ["anaconda", "conda-forge"] [deps] matplotlib = "" -python = ">=3.7,<4" +python = ">=3.7, <3.13" # we get segaults on 3.13 on Apple Silicon scipy = ">=1.9" [pip.deps] diff --git a/test/runtests.jl b/test/runtests.jl index f5de00e..2d0f056 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,7 +49,7 @@ function mne_topoplot(fig, data, positions) end function compare_to_mne(data, positions; kw...) - f, ax, pl = TopoPlots.eeg_topoplot(data, nothing; + f, ax, pl = TopoPlots.eeg_topoplot(data; interpolation=CloughTocher( fill_value = NaN, tol = 0.001, @@ -84,26 +84,26 @@ let f = Makie.Figure(resolution=(1000, 1000)) @test_deprecated interpolation = ClaughTochter() - f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20), - TopoPlots.CHANNELS_10_20; interpolation) + f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20); + labels=TopoPlots.CHANNELS_10_20, interpolation) @test_figure("ClaughTochter", f) end begin # empty eeg topoplot - f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20),TopoPlots.CHANNELS_10_20; interpolation=TopoPlots.NullInterpolator(),) + f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20); labels=TopoPlots.CHANNELS_10_20, interpolation=TopoPlots.NullInterpolator(),) @test_figure("nullInterpolator", f) end begin - f = Makie.Figure(resolution=(1000, 1000)) + f = Makie.Figure(size=(1000, 1000)) s = Makie.Slider(f[:, 1], range=1:size(data, 2), startvalue=351) data_obs = map(s.value) do idx data[:, idx, 1] end TopoPlots.topoplot( f[2, 1], - data_obs, positions, + data_obs, positions; interpolation=DelaunayMesh(), labels = string.(1:length(positions)), colorrange=(-1, 1), @@ -114,7 +114,7 @@ end begin f, ax, pl = TopoPlots.topoplot( - data[:, 340, 1], positions, + data[:, 340, 1], positions; axis=(; aspect=DataAspect()), colorrange=(-1, 1), bounding_geometry = Rect, @@ -183,3 +183,9 @@ begin lines!(ax, rect_extended, color=:red) @test_figure("test-extrapolate-data-circle", f) end + + +begin + f = TopoPlots.eeg_topoplot(1:10; labels=TopoPlots.CHANNELS_10_20[1:10], label_text=true) + @test_figure("test-eeg-channel-labels", f) +end