Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eeg_topoplot labels fix #54

Merged
merged 14 commits into from
Nov 15, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
test/test_images
test/Manifest.toml
docs/Manifest.toml
.CondaPkg/
docs/Manifest-v*.toml
deps/build.log
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 3 additions & 3 deletions docs/src/eeg.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ 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:

```@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
Expand Down
53 changes: 39 additions & 14 deletions src/eeg.jl
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,32 +17,41 @@ 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
points = decompose(Point2f, circle)
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

Expand All @@ -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

"""
Expand All @@ -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})
behinger marked this conversation as resolved.
Show resolved Hide resolved
# 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...)
Expand Down
2 changes: 1 addition & 1 deletion test/CondaPkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
20 changes: 13 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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
Loading