Skip to content

Instantly share code, notes, and snippets.

@behinger
Created February 5, 2023 12:52
Show Gist options
  • Save behinger/8df41a3e051979a8e8ee0068f1aac6b8 to your computer and use it in GitHub Desktop.
Save behinger/8df41a3e051979a8e8ee0068f1aac6b8 to your computer and use it in GitHub Desktop.
"""
dodge(x,y,args...;[plot_fun=scatter!,kwargs...])
dodges plot items if they are on the same `x` (or `y` if specified) value.
## Attributes
### Specific to `dodge`
- `plot_fun::Function = scatter!` in place plotting function to be called after dodging the x/y values
- `dodge::Vector{Int} = Makie.automatic`. Indicates to which group each point belongs, e.g. [1, 1, 2, 3, 2, 2]
- `n_dodge::Int = Makie.automatic` maximal number of dodglings. This indicates over how many "dodglings" `dodge_width` should be split
- `dodge_width::Real=0.1` how much should each x be dodged maximally to the left and right (typically a proportion between 0 and 1)
- `dodge_y::Bool=false` should we dodge x, or rather y?
### Generic
All other arguments are simply forwarded to the `plot_fun`
### Example
```julia
x = [1,3,4, 1,3,4., 4,3,1]
y = [1.1,1.9,2.8,1,2,3,3.05,2.05,1.05]
c = ["red","red","red","green","green","green","blue","blue","blue"]
low,high = repeat([0.2],length(x))
dodge(x,y,low,high;plot_fun=errorbars!,color=c)
dodge!(x,y;color=c, plot_fun=scatter!)
current_figure()
```
"""
@recipe(Dodge,x,y) do scene
Theme(;
#default_theme(scene, fun)...,
#dodge = MakieCore.automatic, # grouping vector what to dodge
#n_dodge = MakieCore.automatic,
plot_fun = scatter!,
dodge=Makie.automatic, # dodge grouping
n_dodge = Makie.automatic, # how many elements to dodge per X
dodge_width = 0.1, # in % diff
dodge_y = false, # dodge in y instead of x direction
)
end
function CairoMakie.plot!(sc::Dodge)
N = length(sc)
@extract(sc,(plot_fun,dodge_y,dodge_width,dodge,n_dodge))
di = dodge_y[] ? sc[:y] : sc[:x]
di = lift(di,dodge,n_dodge,dodge_width) do di,dodge,n_dodge,dodge_width
return dodge_this(di,dodge,n_dodge,dodge_width)
#return x
end
# replace either x or y
x = dodge_y[] ? sc[:x] : di
y = dodge_y[] ? di : sc[:y]
# call the dodged plottingfunction
plot_fun[](sc,x,y,[x for x in sc[3:N]]...;sc.attributes...)
sc
end
"""
get_ndodge(x,dodge::AbstractVector,n_dodge::Makie.Automatic)
get_ndodge(x,dodge::Makie.Automatic,n_dodge::Makie.Automatic)
get_ndodge(x,dodge,n_dodge::Int) = n_dodge
extracts n_dodge. Three possibilities:
1) n_dodge is specified, simply returns it
2) dodge is a vector and specified, we return maximum(dodge)
3) w calculate n_dodge by countinng unique x-values
"""
get_ndodge(x,dodge::AbstractVector,n_dodge::Makie.Automatic) = maximum(dodge)
function get_ndodge(x,dodge::Makie.Automatic,n_dodge::Makie.Automatic)
un = unique(x)
un_c = [count(==(element),x) for element in un ]
n_dodge = maximum(un_c)
return n_dodge
end
get_ndodge(x,dodge,n_dodge::Int) = n_dodge
"""
dodge_this(x::AbstractVector,dodge,n_dodge,dodge_width::Real)
Adds a tiny bit (`(dodge_width * Δ)/n_dodge`) to each dataentry `x`
- `x` typically x or y values (if `dodge_y = true` in `dodge!`)
- `dodge` either `Makie.automatic` or a Vector of Integers. Indicates to which group each point in `x` belongs, e.g. [1 1 2 3 2 2]
- `n_dodge` maximal number of dodglings
- `dodge_width` how much should each x be dodged maximally to the left and right (proportion)
Δ is a unspecified quantity that relates to the distances between x. if `dodge` is provided, `Δ=1`. If `dodge` is automatic, we calculate the distance between the x -values using `abs.(diff(sort(unique(x))))` and use that distance. If different distances are found, we will use the maximal distance to calculate Δ
"""
function dodge_this(x::AbstractVector,dodge,n_dodge,dodge_width::Real)
# first find out what the maximal dodge-number is
n_dodge=get_ndodge(x,dodge,n_dodge)
if n_dodge == 1
# nothing to do here
@warn "nothing to dodge, n_dodge was $n_dodge"
return x
end
# get the distances Δ(xᵢ,xᵢ₊₁)
if isa(dodge,Makie.Automatic)
# we need to "unsort" because it is not always the case that the identical x-values are next to each other
Δ = unique(abs.(diff(sort(unique(x)))))
if length(Δ)>1
Δ = maximum(Δ)
@warn "position values are not equally distanced, using the maximal distance to apply dodge_width to: Δ*dodge_width=$Δ*$dodge_width"
end
#ix = sortperm(x)
#dodge = invperm(ix)
un_x = unique(x)
ix = map(y->findall(y.==x),un_x)
dodge = zeros(Int,length(x))
for i = 1:length(ix)
dodge[ix[i]].=1:length(ix[i])
end
else
Δ = 1
end
@assert length(dodge)==1 || (length(x) == length(dodge)) "length(x) $(length(x)), and length(dodge) $(length(dodge)) should be equal, dodge should be only a single dodge-'category'"
# this generates the normalized dodge-distance between -1 and 1, scales it by the calculated Δ and repeats it for each unique x-entrances. Finally we have to "shuffle" it, in case x is not sorted.
Δx=(range(-1,1,length=n_dodge) .* (dodge_width*Δ/n_dodge))[dodge]
x = x .+ Δx
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment