diff --git a/assets/1005.tsv b/assets/1005.tsv new file mode 100644 index 0000000..dda25d5 --- /dev/null +++ b/assets/1005.tsv @@ -0,0 +1,349 @@ +label x y +AF1 -0.1371 0.6754 +AF10 0.8580 1.1810 +AF10h 0.7078 0.9743 +AF1h -0.0683 0.6700 +AF2 0.1371 0.6754 +AF2h 0.0683 0.6700 +AF3 -0.2781 0.6983 +AF3h -0.2069 0.6848 +AF4 0.2781 0.6983 +AF4h 0.2069 0.6848 +AF5 -0.4269 0.7404 +AF5h -0.3513 0.7166 +AF6 0.4269 0.7404 +AF6h 0.3513 0.7166 +AF7 -0.5878 0.8090 +AF7h -0.5056 0.7708 +AF8 0.5878 0.8090 +AF8h 0.5056 0.7708 +AF9 -0.8580 1.1810 +AF9h -0.7078 0.9743 +AFF1 -0.1588 0.5428 +AFF10 1.0322 1.0322 +AFF10h 0.8515 0.8515 +AFF1h -0.0790 0.5366 +AFF2 0.1588 0.5428 +AFF2h 0.0790 0.5366 +AFF3 -0.3244 0.5692 +AFF3h -0.2403 0.5535 +AFF4 0.3244 0.5692 +AFF4h 0.2403 0.5535 +AFF5 -0.5042 0.6197 +AFF5h -0.4120 0.5908 +AFF6 0.5042 0.6197 +AFF6h 0.4120 0.5908 +AFF7 -0.7071 0.7071 +AFF7h -0.6021 0.6576 +AFF8 0.7071 0.7071 +AFF8h 0.6021 0.6576 +AFF9 -1.0322 1.0322 +AFF9h -0.8515 0.8515 +AFFz 0.0000 0.5345 +AFp1 -0.1089 0.8246 +AFp10 0.6626 1.3007 +AFp10h 0.5467 1.0730 +AFp1h -0.0543 0.8217 +AFp2 0.1089 0.8246 +AFp2h 0.0543 0.8217 +AFp3 -0.2195 0.8369 +AFp3h -0.1639 0.8296 +AFp4 0.2195 0.8369 +AFp4h 0.1639 0.8296 +AFp5 -0.3339 0.8583 +AFp5h -0.2761 0.8463 +AFp6 0.3339 0.8583 +AFp6h 0.2761 0.8463 +AFp7 -0.4540 0.8910 +AFp7h -0.3931 0.8731 +AFp8 0.4540 0.8910 +AFp8h 0.3931 0.8731 +AFp9 -0.6626 1.3007 +AFp9h -0.5467 1.0730 +AFpz 0.0000 0.8207 +AFz 0.0000 0.6682 +C1 -0.1965 0.0000 +C1h -0.0973 0.0000 +C2 0.1965 0.0000 +C2h 0.0973 0.0000 +C3 -0.4088 0.0000 +C3h -0.2996 0.0000 +C4 0.4088 0.0000 +C4h 0.2996 0.0000 +C5 -0.6582 0.0000 +C5h -0.5271 0.0000 +C6 0.6582 0.0000 +C6h 0.5271 0.0000 +CCP1 -0.1976 -0.1008 +CCP1h -0.0978 -0.0991 +CCP2 0.1976 -0.1008 +CCP2h 0.0978 -0.0991 +CCP3 -0.4110 -0.1085 +CCP3h -0.3012 -0.1039 +CCP4 0.4110 -0.1085 +CCP4h 0.3012 -0.1039 +CCP5 -0.6620 -0.1244 +CCP5h -0.5300 -0.1151 +CCP6 0.6620 -0.1244 +CCP6h 0.5300 -0.1151 +CCPz 0.0000 -0.0985 +CP1 -0.1933 -0.2034 +CP1h -0.0958 -0.2000 +CP2 0.1933 -0.2034 +CP2h 0.0958 -0.2000 +CP3 -0.4013 -0.2183 +CP3h -0.2944 -0.2094 +CP4 0.4013 -0.2183 +CP4h 0.2944 -0.2094 +CP5 -0.6433 -0.2489 +CP5h -0.5164 -0.2311 +CP6 0.6433 -0.2489 +CP6h 0.5164 -0.2311 +CPP1 -0.1859 -0.3098 +CPP1h -0.0922 -0.3050 +CPP2 0.1859 -0.3098 +CPP2h 0.0922 -0.3050 +CPP3 -0.3843 -0.3307 +CPP3h -0.2826 -0.3181 +CPP4 0.3843 -0.3307 +CPP4h 0.2826 -0.3181 +CPP5 -0.6113 -0.3731 +CPP5h -0.4930 -0.3486 +CPP6 0.6113 -0.3731 +CPP6h 0.4930 -0.3486 +CPPz 0.0000 -0.3034 +CPz 0.0000 -0.1989 +Cz 0.0000 0.0000 +F1 -0.1747 0.4220 +F10 1.1810 0.8580 +F10h 0.9743 0.7078 +F1h -0.0867 0.4161 +F2 0.1747 0.4220 +F2h 0.0867 0.4161 +F3 -0.3592 0.4472 +F3h -0.2650 0.4321 +F4 0.3592 0.4472 +F4h 0.2650 0.4321 +F5 -0.5653 0.4970 +F5h -0.4586 0.4683 +F6 0.5653 0.4970 +F6h 0.4586 0.4683 +F7 -0.8090 0.5878 +F7h -0.6812 0.5356 +F8 0.8090 0.5878 +F8h 0.6812 0.5356 +F9 -1.1810 0.8580 +F9h -0.9743 0.7078 +FC1 -0.1933 0.2034 +FC1h -0.0958 0.2000 +FC2 0.1933 0.2034 +FC2h 0.0958 0.2000 +FC3 -0.4013 0.2183 +FC3h -0.2944 0.2094 +FC4 0.4013 0.2183 +FC4h 0.2944 0.2094 +FC5 -0.6433 0.2489 +FC5h -0.5164 0.2311 +FC6 0.6433 0.2489 +FC6h 0.5164 0.2311 +FCC1 -0.1976 0.1008 +FCC1h -0.0978 0.0991 +FCC2 0.1976 0.1008 +FCC2h 0.0978 0.0991 +FCC3 -0.4110 0.1085 +FCC3h -0.3012 0.1039 +FCC4 0.4110 0.1085 +FCC4h 0.3012 0.1039 +FCC5 -0.6620 0.1244 +FCC5h -0.5300 0.1151 +FCC6 0.6620 0.1244 +FCC6h 0.5300 0.1151 +FCCz 0.0000 0.0985 +FCz 0.0000 0.1989 +FFC1 -0.1859 0.3098 +FFC1h -0.0922 0.3050 +FFC2 0.1859 0.3098 +FFC2h 0.0922 0.3050 +FFC3 -0.3843 0.3307 +FFC3h -0.2826 0.3181 +FFC4 0.3843 0.3307 +FFC4h 0.2826 0.3181 +FFC5 -0.6113 0.3731 +FFC5h -0.4930 0.3486 +FFC6 0.6113 0.3731 +FFC6h 0.4930 0.3486 +FFCz 0.0000 0.3034 +FFT10 1.3007 0.6626 +FFT10h 1.0730 0.5467 +FFT7 -0.8910 0.4540 +FFT7h -0.7425 0.4070 +FFT8 0.8910 0.4540 +FFT8h 0.7425 0.4070 +FFT9 -1.3007 0.6626 +FFT9h -1.0730 0.5467 +FT10 1.3884 0.4512 +FT10h 1.1453 0.3722 +FT7 -0.9511 0.3090 +FT7h -0.7860 0.2738 +FT8 0.9511 0.3090 +FT8h 0.7860 0.2738 +FT9 -1.3884 0.4512 +FT9h -1.1453 0.3722 +FTT10 1.4418 0.2284 +FTT10h 1.1894 0.1884 +FTT7 -0.9877 0.1564 +FTT7h -0.8120 0.1376 +FTT8 0.9877 0.1564 +FTT8h 0.8120 0.1376 +FTT9 -1.4418 0.2284 +FTT9h -1.1894 0.1884 +Fp1 -0.3090 0.9511 +Fp1h -0.1564 0.9877 +Fp2 0.3090 0.9511 +Fp2h 0.1564 0.9877 +Fpz 0.0000 1.0000 +Fz 0.0000 0.4142 +I1 -0.4512 -1.3884 +I1h -0.2284 -1.4418 +I2 0.4512 -1.3884 +I2h 0.2284 -1.4418 +Iz 0.0000 -1.4598 +LPA -1.4598 -0.0000 +N1 -0.4512 1.3884 +N1h -0.2284 1.4418 +N2 0.4512 1.3884 +N2h 0.2284 1.4418 +NAS -0.0000 1.4598 +NFp1 -0.3722 1.1453 +NFp1h -0.1884 1.1894 +NFp2 0.3722 1.1453 +NFp2h 0.1884 1.1894 +NFpz -0.0000 1.2042 +Nz -0.0000 1.4598 +O1 -0.3090 -0.9511 +O1h -0.1564 -0.9877 +O2 0.3090 -0.9511 +O2h 0.1564 -0.9877 +OI1 -0.3722 -1.1453 +OI1h -0.1884 -1.1894 +OI2 0.3722 -1.1453 +OI2h 0.1884 -1.1894 +OIz 0.0000 -1.2042 +Oz 0.0000 -1.0000 +P1 -0.1747 -0.4220 +P10 1.1810 -0.8580 +P10h 0.9743 -0.7078 +P1h -0.0867 -0.4161 +P2 0.1747 -0.4220 +P2h 0.0867 -0.4161 +P3 -0.3592 -0.4472 +P3h -0.2650 -0.4321 +P4 0.3592 -0.4472 +P4h 0.2650 -0.4321 +P5 -0.5653 -0.4970 +P5h -0.4586 -0.4683 +P6 0.5653 -0.4970 +P6h 0.4586 -0.4683 +P7 -0.8090 -0.5878 +P7h -0.6812 -0.5356 +P8 0.8090 -0.5878 +P8h 0.6812 -0.5356 +P9 -1.1810 -0.8580 +P9h -0.9743 -0.7078 +PO1 -0.1371 -0.6754 +PO10 0.8580 -1.1810 +PO10h 0.7078 -0.9743 +PO1h -0.0683 -0.6700 +PO2 0.1371 -0.6754 +PO2h 0.0683 -0.6700 +PO3 -0.2781 -0.6983 +PO3h -0.2069 -0.6848 +PO4 0.2781 -0.6983 +PO4h 0.2069 -0.6848 +PO5 -0.4269 -0.7404 +PO5h -0.3513 -0.7166 +PO6 0.4269 -0.7404 +PO6h 0.3513 -0.7166 +PO7 -0.5878 -0.8090 +PO7h -0.5056 -0.7708 +PO8 0.5878 -0.8090 +PO8h 0.5056 -0.7708 +PO9 -0.8580 -1.1810 +PO9h -0.7078 -0.9743 +POO1 -0.1089 -0.8246 +POO10 0.6626 -1.3007 +POO10h 0.5467 -1.0730 +POO1h -0.0543 -0.8217 +POO2 0.1089 -0.8246 +POO2h 0.0543 -0.8217 +POO3 -0.2195 -0.8369 +POO3h -0.1639 -0.8296 +POO4 0.2195 -0.8369 +POO4h 0.1639 -0.8296 +POO5 -0.3339 -0.8583 +POO5h -0.2761 -0.8463 +POO6 0.3339 -0.8583 +POO6h 0.2761 -0.8463 +POO7 -0.4540 -0.8910 +POO7h -0.3931 -0.8731 +POO8 0.4540 -0.8910 +POO8h 0.3931 -0.8731 +POO9 -0.6626 -1.3007 +POO9h -0.5467 -1.0730 +POOz 0.0000 -0.8207 +POz 0.0000 -0.6682 +PPO1 -0.1588 -0.5428 +PPO10 1.0322 -1.0322 +PPO10h 0.8515 -0.8515 +PPO1h -0.0790 -0.5366 +PPO2 0.1588 -0.5428 +PPO2h 0.0790 -0.5366 +PPO3 -0.3244 -0.5692 +PPO3h -0.2403 -0.5535 +PPO4 0.3244 -0.5692 +PPO4h 0.2403 -0.5535 +PPO5 -0.5042 -0.6197 +PPO5h -0.4120 -0.5908 +PPO6 0.5042 -0.6197 +PPO6h 0.4120 -0.5908 +PPO7 -0.7071 -0.7071 +PPO7h -0.6021 -0.6576 +PPO8 0.7071 -0.7071 +PPO8h 0.6021 -0.6576 +PPO9 -1.0322 -1.0322 +PPO9h -0.8515 -0.8515 +PPOz 0.0000 -0.5345 +Pz 0.0000 -0.4142 +RPA 1.4598 0.0000 +T10 1.4598 0.0000 +T10h 1.2042 0.0000 +T7 -1.0000 0.0000 +T7h -0.8072 0.0000 +T8 1.0000 0.0000 +T8h 0.8072 -0.0000 +T9 -1.4598 -0.0000 +T9h -1.2042 -0.0000 +TP10 1.3884 -0.4512 +TP10h 1.1453 -0.3722 +TP7 -0.9511 -0.3090 +TP7h -0.7860 -0.2738 +TP8 0.9511 -0.3090 +TP8h 0.7860 -0.2738 +TP9 -1.3884 -0.4512 +TP9h -1.1453 -0.3722 +TPP10 1.3007 -0.6626 +TPP10h 1.0730 -0.5467 +TPP7 -0.8910 -0.4540 +TPP7h -0.7425 -0.4070 +TPP8 0.8910 -0.4540 +TPP8h 0.7425 -0.4070 +TPP9 -1.3007 -0.6626 +TPP9h -1.0730 -0.5467 +TTP10 1.4418 -0.2284 +TTP10h 1.1894 -0.1884 +TTP7 -0.9877 -0.1564 +TTP7h -0.8120 -0.1376 +TTP8 0.9877 -0.1564 +TTP8h 0.8120 -0.1376 +TTP9 -1.4418 -0.2284 +TTP9h -1.1894 -0.1884 \ No newline at end of file diff --git a/assets/layout_10_05.bin b/assets/layout_10_05.bin new file mode 100644 index 0000000..e828a23 Binary files /dev/null and b/assets/layout_10_05.bin differ diff --git a/assets/write_10_20-bin.jl b/assets/write_template-positions-bin.jl similarity index 56% rename from assets/write_10_20-bin.jl rename to assets/write_template-positions-bin.jl index c83b62f..726d829 100644 --- a/assets/write_10_20-bin.jl +++ b/assets/write_template-positions-bin.jl @@ -1,6 +1,13 @@ -using PyMNE, PyCall, TopoPlots +using PyMNE, PythonCall, TopoPlots channels = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", "fp2", "f4", "c4", "p4", "o2", "f8", "t4", "t6"] info = pycall(PyMNE.mne.create_info, PyObject, channels, 120.0; ch_types="eeg") info.set_montage("standard_1020"; match_case=false) layout = PyMNE.mne.find_layout(info) write(TopoPlots.assetpath("layout_10_20.bin"), hcat(layout.pos[:, 1], layout.pos[:, 2])) + +#--- + +using CSV,DataFrames + +loc2d = CSV.read(TopoPlots.assetpath("1005.tsv"),DataFrame) # taken from https://github.com/sappelhoff/eeg_positions/blob/main/data/Fpz-T8-Oz-T7/standard_1005_2D.tsv +write(TopoPlots.assetpath("layout_10_05.bin"),hcat(loc2d.x,loc2d.y)) diff --git a/docs/make.jl b/docs/make.jl index 7f28641..f9b7d12 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,12 +1,15 @@ using TopoPlots using Documenter +using TopoPlots: CHANNEL_TO_POSITION_10_05, CHANNEL_TO_POSITION_10_20 + DocMeta.setdocmeta!(TopoPlots, :DocTestSetup, :(using TopoPlots); recursive=true) makedocs(; modules=[TopoPlots], authors="Benedikt Ehinger, Simon Danisch, Beacon Biosignals", sitename="TopoPlots.jl", + checkdocs=:exports, format=Documenter.HTML(; prettyurls=get(ENV, "CI", "false") == "true", canonical="https://MakieOrg.github.io/TopoPlots.jl", diff --git a/docs/src/eeg.md b/docs/src/eeg.md index 301ebcc..86a6eaf 100644 --- a/docs/src/eeg.md +++ b/docs/src/eeg.md @@ -9,15 +9,18 @@ TopoPlots.eeg_topoplot -For the standard 10/20 montage, one can drop the `positions` attribute: +For the standard 10/20 (or 10/05) 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,)) +labels = TopoPlots.CHANNELS_10_05 # TopoPlots.CHANNELS_10_20 contains the 10/20 subset + +f,ax,h = TopoPlots.eeg_topoplot(rand(348); labels=labels, axis=(aspect=DataAspect(),), label_text=true, label_scatter=(markersize=2, strokewidth=2,),colorrange=[-5,5]) + ``` -If the channels aren't 10/20, one can still plot them, but then the positions need to be passed as well: + +If the channels aren't 10/05, one can still plot them, but then the positions need to be passed as well: ```@example eeg data, positions = TopoPlots.example_data() @@ -25,7 +28,6 @@ labels = ["s$i" for i in 1:size(data, 1)] TopoPlots.eeg_topoplot(data[:, 340, 1]; labels, label_text = true, positions=positions, axis=(aspect=DataAspect(),)) ``` - ## Subset of channels If you only ask to plot a subset of channels, we highly recommend to define your bounding geometry yourself. We follow MNE functionality and normalize the positions prior to interpolation / plotting. If you only use a subset of channels, the positions will be relative to each other, not at absolute coordinates. diff --git a/docs/src/functions.md b/docs/src/functions.md index 3b35467..6aa34dd 100644 --- a/docs/src/functions.md +++ b/docs/src/functions.md @@ -4,4 +4,3 @@ TopoPlots.enclosing_geometry TopoPlots.labels2positions ``` - diff --git a/docs/src/topo_series.jl b/docs/src/topo_series.jl new file mode 100644 index 0000000..0f841aa --- /dev/null +++ b/docs/src/topo_series.jl @@ -0,0 +1,180 @@ +### A Pluto.jl notebook ### +# v0.19.9 + +using Markdown +using InteractiveUtils + +# ╔═╡ 2fafb0da-f3a9-11ec-0ddf-6725344070fe +begin +using Pkg +Pkg.activate("../../devEnv") # docs +#Pkg.add("PyMNE") +#Pkg.add(path="../../../TopoPlotsjl/") +Pkg.develop(path="../../../TopoPlotsjl/") +#Pkg.add("DataFrames") +#Pkg.add("AlgebraOfGraphics") + #Pkg.add("StatsBase") + #Pkg.add("CategoricalArrays") + + #Pkg.add("JLD2") + + #Pkg.add("CairoMakie") +end + +# ╔═╡ c4a25915-c7f5-453a-a4f0-4b40ebedea4c +using Revise + +# ╔═╡ 59b87673-02d2-4deb-90be-74d923d170eb + using TopoPlots + + +# ╔═╡ 452a245c-773a-4303-a970-f2592c3e879f +begin + #using TopoPlots + #using ../../../Topoplotsjl + using CairoMakie + using DataFrames + + +end + + +# ╔═╡ 77dc1ba9-9484-485b-a49d-9aa231ef4983 +using Statistics + +# ╔═╡ 311f10ff-deb8-4f82-8b12-d5b643656828 +using PyMNE + +# ╔═╡ 9fa5c598-3578-4989-9585-29fd32ae1056 +using Distributions + +# ╔═╡ 6cda29dc-7086-4079-83c6-3650204a82ff +pathof(TopoPlots) + +# ╔═╡ e0cc560f-d3e8-415b-b22d-6bca23ef093c +revise(TopoPlots) + +# ╔═╡ f4b81740-d907-42ae-a0df-f46fb2f2cb15 +begin + +data = Array{Float32}(undef, 64, 400, 3) +#read!(TopoPlots.assetpath("example-data.bin"), data) + read!(splitdir(pathof(TopoPlots))[1]*"/../assets/example-data.bin",data) + +positions = Vector{Point2f}(undef, 64) + read!(splitdir(pathof(TopoPlots))[1]*"/../assets/layout64.bin",positions) +#read!(TopoPlots.assetpath("layout64.bin"), positions) + +end; + + +# ╔═╡ 42f7755b-80f4-4185-8d21-42e11730e0fc +begin + using Random + pos = positions[1:10] +eeg_topoplot(rand(MersenneTwister(1),length(pos)), string.(1:length(pos));positions=pos,pad_value=0.) +end + +# ╔═╡ aad784ee-6bb7-4f3c-8444-be050456ddea +eeg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions) + +# ╔═╡ 237e4f4a-cdf2-4bac-8096-de8050251745 +eeg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions,pad_value=0.1) + +# ╔═╡ f522329b-3653-4059-9955-8cd05570e923 +topoplot(rand(MersenneTwister(1),length(pos)),pos) + +# ╔═╡ a9d2a2e2-6c8c-4cfc-9fed-b5e082cb44af +let +mon = PyMNE.channels.make_standard_montage("standard_1020") + +posMat = (Matrix(hcat(pos...)).-0.5).*0.5 + #pos = PyMNE.channels.make_eeg_layout(mon).pos +PyMNE.viz.plot_topomap(rand(MersenneTwister(1),length(pos)),posMat',cmap="RdBu_r",extrapolate="box",border=-1) +end + +# ╔═╡ c358633f-8d18-4c5e-80f7-ab972e8860be +Pkg.status("TopoPlots") + +# ╔═╡ c0a2ad2e-ccce-4e80-b52c-75f1428ed182 +e1eg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions,interpolation = TopoPlots.NormalMixtureInterpolator() ) + +# ╔═╡ d7620a42-d54c-4244-a820-d15aecdae626 +@time TopoPlots.eeg_topoplot_series(data[:,:,1],40;topoplotCfg=(positions=positions,label_scatter=false)) + +# ╔═╡ ec59c704-ae33-4a62-82ce-63acc6b17793 +f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20),TopoPlots.CHANNELS_10_20; interpolation=TopoPlots.NullInterpolator(),) + +# ╔═╡ f3d1f3cc-f7c9-4ef4-ba4f-3d32f2509cad +let + # 4 coordinates with one peak + positions = Point2f[(-1, 0), (0, -1), (1, 0), (0, 1)] + i = 1 + peak_xy = positions[i] + data = zeros(length(positions)) + data[i] = 1.1 + fig = topoplot(data, positions) + # tighten the limits so that the limits of the axis and the data will match + tightlimits!(fig.axis) + + # retrieve the interpolated data + m = fig.plot.plots[].color[] + # get the limits of the axes and data + rect = fig.axis.targetlimits[] + minx, miny = minimum(rect) + maxx, maxy = maximum(rect) + # recreate the coordinates of the data + x = range(minx, maxx, length=size(m, 1)) + y = range(miny, maxy, length=size(m, 2)) + xys = Point2f.(x, y') + + # find the highest point + _, i = findmax(x -> isnan(x) ? -Inf : x, m) + xy = xys[i] + @show peak_xy + @show xy + #@test isapprox(xy, peak_xy; atol=0.02) + @show isapprox(xy, peak_xy; atol=0.02) + fig +end + +# ╔═╡ 872ac6a4-ddaa-4dfb-a40d-9d5ea55bdb3d +let + f = Figure() + axis = Axis(f[1, 1], aspect = 1) + xlims!(low = -2, high = 2) + ylims!(low = -2, high = 2) + + data = [0, 0, 0] + pos1 = [Point2f(-1, -1), Point2f(-1.0, 0.0), Point2f(0, -1)] + pos2 = [Point2f(1, 1), Point2f(1.0, 0.0), Point2f(0, 1)] + + pos1 = pos1 .- mean(pos1) + pos2 = pos2 .- mean(pos2) + eeg_topoplot!(axis, data, positions=pos1) + eeg_topoplot!(axis, data, positions=pos2) + f +end + +# ╔═╡ Cell order: +# ╠═2fafb0da-f3a9-11ec-0ddf-6725344070fe +# ╠═6cda29dc-7086-4079-83c6-3650204a82ff +# ╠═c4a25915-c7f5-453a-a4f0-4b40ebedea4c +# ╠═e0cc560f-d3e8-415b-b22d-6bca23ef093c +# ╠═59b87673-02d2-4deb-90be-74d923d170eb +# ╠═452a245c-773a-4303-a970-f2592c3e879f +# ╠═f4b81740-d907-42ae-a0df-f46fb2f2cb15 +# ╠═77dc1ba9-9484-485b-a49d-9aa231ef4983 +# ╠═aad784ee-6bb7-4f3c-8444-be050456ddea +# ╠═237e4f4a-cdf2-4bac-8096-de8050251745 +# ╠═42f7755b-80f4-4185-8d21-42e11730e0fc +# ╠═f522329b-3653-4059-9955-8cd05570e923 +# ╠═311f10ff-deb8-4f82-8b12-d5b643656828 +# ╠═a9d2a2e2-6c8c-4cfc-9fed-b5e082cb44af +# ╠═c358633f-8d18-4c5e-80f7-ab972e8860be +# ╠═9fa5c598-3578-4989-9585-29fd32ae1056 +# ╠═c0a2ad2e-ccce-4e80-b52c-75f1428ed182 +# ╠═d7620a42-d54c-4244-a820-d15aecdae626 +# ╠═ec59c704-ae33-4a62-82ce-63acc6b17793 +# ╠═f3d1f3cc-f7c9-4ef4-ba4f-3d32f2509cad +# ╠═872ac6a4-ddaa-4dfb-a40d-9d5ea55bdb3d diff --git a/src/eeg.jl b/src/eeg.jl index 37c953d..c5bcb08 100644 --- a/src/eeg.jl +++ b/src/eeg.jl @@ -8,6 +8,7 @@ default_theme(scene, TopoPlot)..., label_scatter=true, contours=true, + enlarge=1, ) end @@ -22,9 +23,13 @@ Attributes: # Some attributes from topoplot are set to different defaults: * `label_scatter = true` * `contours = true` +* `enlarge = 1`` Otherwise the recipe just uses the [`topoplot`](@ref) defaults and passes through the attributes. +!!! note + The 10-05 channel locations are "perfect" spherical locations based on https://github.com/sappelhoff/eeg_positions/ - the mne-default 10-20 locations are _not_, they were warped to a fsaverage head. Which makes the locations provided here good for visualizations, but not good for source localisation. + !!! note You MUST set `label_text=true` for labels to display. """ @@ -59,9 +64,97 @@ function draw_ear_nose!(parent, circle; kw...) end + +const CHANNELS_10_05 = ["af1","af10","af10h","af1h","af2","af2h","af3","af3h", + "af4","af4h","af5","af5h","af6","af6h","af7","af7h","af8", + "af8h","af9","af9h","aff1","aff10","aff10h","aff1h","aff2", + "aff2h","aff3","aff3h","aff4","aff4h","aff5","aff5h","aff6", + "aff6h","aff7","aff7h","aff8","aff8h","aff9","aff9h","affz", + "afp1","afp10","afp10h","afp1h","afp2","afp2h","afp3","afp3h", + "afp4","afp4h","afp5","afp5h","afp6","afp6h","afp7","afp7h", + "afp8","afp8h","afp9","afp9h","afpz","afz","c1","c1h","c2", + "c2h","c3","c3h","c4","c4h","c5","c5h","c6","c6h","ccp1", + "ccp1h","ccp2","ccp2h","ccp3","ccp3h","ccp4","ccp4h","ccp5", + "ccp5h","ccp6","ccp6h","ccpz","cp1","cp1h","cp2","cp2h","cp3", + "cp3h","cp4","cp4h","cp5","cp5h","cp6","cp6h","cpp1","cpp1h", + "cpp2","cpp2h","cpp3","cpp3h","cpp4","cpp4h","cpp5","cpp5h", + "cpp6","cpp6h","cppz","cpz","cz","f1","f10","f10h","f1h", + "f2","f2h","f3","f3h","f4","f4h","f5","f5h","f6","f6h", + "f7","f7h","f8","f8h","f9","f9h","fc1","fc1h","fc2","fc2h", + "fc3","fc3h","fc4","fc4h","fc5","fc5h","fc6","fc6h","fcc1", + "fcc1h","fcc2","fcc2h","fcc3","fcc3h","fcc4","fcc4h","fcc5", + "fcc5h","fcc6","fcc6h","fccz","fcz","ffc1","ffc1h","ffc2", + "ffc2h","ffc3","ffc3h","ffc4","ffc4h","ffc5","ffc5h","ffc6", + "ffc6h","ffcz","fft10","fft10h","fft7","fft7h","fft8","fft8h", + "fft9","fft9h","ft10","ft10h","ft7","ft7h","ft8","ft8h","ft9", + "ft9h","ftt10","ftt10h","ftt7","ftt7h","ftt8","ftt8h","ftt9", + "ftt9h","fp1","fp1h","fp2","fp2h","fpz","fz","i1","i1h","i2", + "i2h","iz","lpa","n1","n1h","n2","n2h","nas","nfp1","nfp1h", + "nfp2","nfp2h","nfpz","nz","o1","o1h","o2","o2h","oi1","oi1h", + "oi2","oi2h","oiz","oz","p1","p10","p10h","p1h","p2","p2h", + "p3","p3h","p4","p4h","p5","p5h","p6","p6h","p7","p7h","p8", + "p8h","p9","p9h","po1","po10","po10h","po1h","po2","po2h", + "po3","po3h","po4","po4h","po5","po5h","po6","po6h","po7", + "po7h","po8","po8h","po9","po9h","poo1","poo10","poo10h", + "poo1h","poo2","poo2h","poo3","poo3h","poo4","poo4h","poo5", + "poo5h","poo6","poo6h","poo7","poo7h","poo8","poo8h","poo9", + "poo9h","pooz","poz","ppo1","ppo10","ppo10h","ppo1h","ppo2", + "ppo2h","ppo3","ppo3h","ppo4","ppo4h","ppo5","ppo5h","ppo6", + "ppo6h","ppo7","ppo7h","ppo8","ppo8h","ppo9","ppo9h","ppoz", + "pz","rpa","t10","t10h","t7","t7h","t8","t8h","t9","t9h", + "tp10","tp10h","tp7","tp7h","tp8","tp8h","tp9","tp9h","tpp10", + "tpp10h","tpp7","tpp7h","tpp8","tpp8h","tpp9","tpp9h","ttp10", + "ttp10h","ttp7","ttp7h","ttp8","ttp8h","ttp9","ttp9h"] + +""" + CHANNEL_TO_POSITION_10_05 + +Positions for 10/05 channels. + +These are used for all channel locations, including the 10/20 subset. + +!!! warning + The 10/05 channel locations are "perfect" spherical locations based on + https://github.com/sappelhoff/eeg_positions/, while + the default 10/20 locations in MNE-Python are **not**. + The coordinates here are better for visualization, while the MNE provided + coordinates, which were warped to a fsaverage head are better for source + localization. +""" +const CHANNEL_TO_POSITION_10_05 = let + # We load this during precompilation, so that this gets stored as a global + # that's immediately loaded when loading the package + result = Matrix{Float64}(undef, 348, 2) + read!(assetpath("layout_10_05.bin"), result) + positions = Point2f.(result[:, 1], result[:, 2]) + d = Dict{String,Point2f}(zip(CHANNELS_10_05, positions)) + d["t3"] = d["t7"] + d["t4"] = d["t8"] + d["t5"] = d["p7"] + d["t6"] = d["p8"] + d +end + +# even though these are not actively used, sometimes they can be helpful just to plot a default subset of channels. Therefore we havent deleted them yet (because 10_05 is a superset) const CHANNELS_10_20 = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", "fp2", "f4", "c4", "p4", "o2", "f8", "t4", "t6"] -const CHANNEL_TO_POSITION_10_20 = begin +""" + CHANNEL_TO_POSITION_10_20 + +Positions for 10/20 channels. + +See also [CHANNEL_TO_POSITION_10_05](@ref) + +!!! warning + This is deprecated and unused! The coordinates here differ slightly from the + newer 10/05 coordinates now used in all plotting functions. + The 10-05 channel locations are "perfect" spherical locations based on + https://github.com/sappelhoff/eeg_positions/, while + the default 10/20 locations in MNE-Python are **not**. + The MNE locations were warped to a fsaverage head, which is better for source + localization but worse for visualization. +""" +const CHANNEL_TO_POSITION_10_20 = let # We load this during precompilation, so that this gets stored as a global # that's immediately loaded when loading the package result = Matrix{Float64}(undef, 19, 2) @@ -72,15 +165,15 @@ end """ labels2positions(labels) -Currently only supports 10/20 layout, by looking it up in `TopoPlots.CHANNEL_TO_POSITION_10_20`. +Currently supports 10/20 and 10/05 layout, by looking it up in `TopoPlots.CHANNEL_TO_POSITION_10_05`. """ function labels2positions(labels) return map(labels) do label key = lowercase(label) - if haskey(CHANNEL_TO_POSITION_10_20, key) - return CHANNEL_TO_POSITION_10_20[key] + if haskey(CHANNEL_TO_POSITION_10_05, key) + return CHANNEL_TO_POSITION_10_05[key] else - error("Currently only 10/20 is supported. Found label: $(label)") + error("Currently only 10/05 is supported. Found label: $(label)") end end end @@ -95,7 +188,7 @@ 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") + (!isnothing(labels) && labels != Makie.Automatic) || error("Either positions or labels (10/05-lookup) have to be specified") return labels2positions(labels) else diff --git a/test/runtests.jl b/test/runtests.jl index 2229fe7..09e45bb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -196,3 +196,12 @@ 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 + +let + pos = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_20) + pos10_05 = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_05) + @test pos[TopoPlots.CHANNELS_10_20 .== "t3"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "t7"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t4"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "t8"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t5"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "p7"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t6"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "p8"] +end