diff --git a/.gitignore b/.gitignore index f85cbbe..81a331a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ test/test_images test/Manifest.toml docs/Manifest.toml +docs/.CondaPkg deps/build.log diff --git a/docs/src/eeg.md b/docs/src/eeg.md index 44ab292..baaba71 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 1 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=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 1 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 = labels, label_text = true, positions=positions, axis=(aspect=DataAspect(),)) ``` ```@docs diff --git a/docs/src/general.md b/docs/src/general.md index 1748d0b..4cd7af5 100644 --- a/docs/src/general.md +++ b/docs/src/general.md @@ -42,13 +42,13 @@ using TopoPlots, CairoMakie, ScatteredInterpolation, NaturalNeighbours data, positions = TopoPlots.example_data() -f = Figure(resolution=(1000, 1250)) +f = Figure(size=(1000, 1250)) interpolators = [ DelaunayMesh() CloughTocher(); SplineInterpolator() NullInterpolator(); ScatteredInterpolationMethod(ThinPlate()) ScatteredInterpolationMethod(Shepard(3)); - NaturalNeighboursMethod(Sibson(1)) NaturalNeighboursMethod(Triangle()); + #NaturalNeighboursMethod(Sibson(1)) NaturalNeighboursMethod(Triangle()); ] data_slice = data[:, 360, 1] @@ -146,3 +146,25 @@ TopoPlots.topoplot( label_scatter=(; strokewidth=2), contours=(linewidth=2, color=:white)) ``` + + +## Different plotfunctions + +It is possible to exchange the plotting function, from `heatmap!` to `contourf!` or `surface!`. Due to different keyword arguments, one needs to filter which keywords are passed to the plotting function manually. + +```@example 1 +f = Figure() + +TopoPlots.topoplot(f[1,1], + rand(10), rand(Point2f, 10), + axis=(; aspect=DataAspect()), + plotfnc! = contourf!, plotfnc_kwargs_filter=[:colormap]) + +TopoPlots.topoplot(f[1,2], + rand(10), rand(Point2f, 10), + axis=(; aspect=DataAspect()), + plotfnc! = surface!) # surface can take all default kwargs similar to heatmap! + +f + +``` 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/TopoPlots.jl b/src/TopoPlots.jl index 10085fd..45efce5 100644 --- a/src/TopoPlots.jl +++ b/src/TopoPlots.jl @@ -63,8 +63,8 @@ export GeomExtrapolation, NullExtrapolation @compile_workload begin # all calls in this block will be precompiled, regardless of whether # they belong to your package or not (on Julia 1.8 and higher) - eeg_topoplot(view(data, :, 340, 1); positions) - eeg_topoplot(data[:, 340, 1]; positions) + eeg_topoplot(view(data, :, 340, 1); positions) + eeg_topoplot(data[:, 340, 1]; positions) end end diff --git a/src/core-recipe.jl b/src/core-recipe.jl index eaec5c5..60edc98 100644 --- a/src/core-recipe.jl +++ b/src/core-recipe.jl @@ -13,7 +13,9 @@ labels = nothing, label_text = false, label_scatter = false, - contours = false + contours = false, + plotfnc! = heatmap!, + plotfnc_kwargs_filter= [:colorrange, :colormap, :interpolate], ) end @@ -39,6 +41,8 @@ Creates an irregular interpolation for each `data[i]` point at `positions[i]`. * true: add point for each position with default attributes * NamedTuple: Attributes get passed to the Makie.scatter! call. * `markersize = 5`: size of the points defined by positions, shortcut for label_scatter=(markersize=5,) +* `plotfnc! = heatmap!`: function to use for plotting the interpolation +* `plotfnc_kwargs_filter = [:colorrange, :colormap, :interpolate]`: different `plotfnc` support different kwargs, this array contains the keys to filter the full list which is [:colorrange, :colormap, :interpolate] * `contours = false`: * true: add scatter point for each position @@ -116,8 +120,9 @@ function Makie.plot!(p::TopoPlot) # z[mask] .= NaN return z end - - heatmap!(p, xg, yg, data, colormap=p.colormap, colorrange=colorrange, interpolate=true) + kwargs_all = Dict(:colorrange => colorrange, :colormap => p.colormap, :interpolate => true) + + p.plotfnc![](p, xg, yg, data; (p.plotfnc_kwargs_filter[].=>getindex.(Ref(kwargs_all),p.plotfnc_kwargs_filter[]))...) contours = to_value(p.contours) attributes = @plot_or_defaults contours Attributes(color=(:black, 0.5), linestyle=:dot, levels=6) if !isnothing(attributes) && !(p.interpolation[] isa NullInterpolator) diff --git a/src/eeg.jl b/src/eeg.jl index 9dd8752..d6e2808 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,7 +17,7 @@ end Attributes: * `positions::Vector{<: Point} = Makie.automatic`: Can be calculated from label (channel) names. Currently, only 10/20 montage has default coordinates provided. - +* `labels::Vector{<: String} = Makie.automatic`: Add custom labels, in case `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` @@ -25,7 +26,15 @@ Attributes: Otherwise the recipe just uses the [`topoplot`](@ref) defaults and passes through the attributes. """ eeg_topoplot - + function eeg_topoplot(data,labels;kwargs...) + @warn "labels as positional arguments have been deprecated. Please provide them as keyword arguments" + eeg_topoplot(data;labels=labels,kwargs...) + end + function eeg_topoplot!(fig, data,labels;kwargs...) + @warn "labels as positional arguments have been deprecated. Please provide them as keyword arguments" + eeg_topoplot!(fig,data;labels=labels,kwargs...) + end + function draw_ear_nose!(parent, circle; kw...) # draw circle head_points = lift(circle) do circle @@ -33,15 +42,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 +69,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 +82,40 @@ 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 + @assert !isnothing(labels) && labels != Makie.Automatic "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 - - tplot = topoplot!(plot, Attributes(plot), plot.data, positions; labels=plot.labels) + 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 + plot.labels = 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/runtests.jl b/test/runtests.jl index f5de00e..9ada81e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -183,3 +183,10 @@ begin lines!(ax, rect_extended, color=:red) @test_figure("test-extrapolate-data-circle", f) end + +let + data, positions = TopoPlots.example_data() + f, ax, pl = topoplot(1:10,positions[1:10];plotfnc! = contourf!,plotfnc_kwargs_filter=[:colormap]) + f, ax, pl = topoplot(1:10,positions[1:10];plotfnc! = (args...;kwargs...)->heatmap!(args...;alpha=0.3,kwargs...)) + #@test_figure("ClaughTochter", f) +end \ No newline at end of file