Skip to content
This repository was archived by the owner on Jul 13, 2021. It is now read-only.

Commit 185de01

Browse files
authored
Merge pull request #730 from JuliaPlots/pv/violin
support datalimits and respect area in violin
2 parents d19bbfd + d4ce282 commit 185de01

File tree

2 files changed

+108
-32
lines changed

2 files changed

+108
-32
lines changed

docs/src/plotting_functions/violin.md

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,53 @@ ys = randn(1000)
1717
violin(xs, ys)
1818
```
1919

20+
21+
```@example
22+
using CairoMakie
23+
CairoMakie.activate!() # hide
24+
AbstractPlotting.inline!(true) # hide
25+
26+
xs = rand(1:3, 1000)
27+
ys = map(xs) do x
28+
return x == 1 ? randn() : x == 2 ? 0.5 * randn() : 5 * rand()
29+
end
30+
31+
violin(xs, ys, datalimits = extrema)
32+
```
33+
34+
2035
```@example
2136
using CairoMakie
2237
CairoMakie.activate!() # hide
2338
AbstractPlotting.inline!(true) # hide
2439
25-
xs1 = rand(1:3, 1000)
26-
ys1 = randn(1000)
27-
dodge1 = rand(1:2, 1000)
40+
N = 1000
41+
xs = rand(1:3, N)
42+
dodge = rand(1:2, N)
43+
side = rand([:left, :right], N)
44+
color = @. ifelse(side == :left, :orange, :teal)
45+
ys = map(side) do s
46+
return s == :left ? randn() : rand()
47+
end
48+
49+
violin(xs, ys, dodge = dodge, side = side, color = color)
50+
```
51+
52+
```@example
53+
using CairoMakie
54+
CairoMakie.activate!() # hide
55+
AbstractPlotting.inline!(true) # hide
2856
29-
xs2 = rand(1:3, 1000)
30-
ys2 = randn(1000)
31-
dodge2 = rand(1:2, 1000)
57+
N = 1000
58+
xs = rand(1:3, N)
59+
side = rand([:left, :right], N)
60+
color = map(xs, side) do x, s
61+
colors = s == :left ? [:red, :orange, :yellow] : [:blue, :teal, :cyan]
62+
return colors[x]
63+
end
64+
ys = map(side) do s
65+
return s == :left ? randn() : rand()
66+
end
3267
33-
fig = Figure()
34-
ax = Axis(fig[1, 1])
35-
violin!(ax, xs1, ys1, dodge = dodge1, side = :left, color = :orange)
36-
violin!(ax, xs2, ys2, dodge = dodge2, side = :right, color = :teal)
37-
fig
68+
violin(xs, ys, side = side, color = color)
3869
```

src/stats/violin.jl

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Draw a violin plot.
88
- `orientation=:vertical`: orientation of the violins (`:vertical` or `:horizontal`)
99
- `width=0.8`: width of the violin
1010
- `show_median=true`: show median as midline
11+
- `side=:both`: specify `:left` or `:right` to only plot the violin on one side
12+
- `datalimits`: specify values to trim the `violin`. Can be a `Tuple` or a `Function` (e.g. `datalimits=extrema`)
1113
"""
1214
@recipe(Violin, x, y) do scene
1315
Theme(;
@@ -21,7 +23,8 @@ Draw a violin plot.
2123
n_dodge = automatic,
2224
x_gap = 0.2,
2325
dodge_gap = 0.03,
24-
trim = false,
26+
datalimits = (-Inf, Inf),
27+
max_density = automatic,
2528
strokecolor = :white,
2629
show_median = false,
2730
mediancolor = automatic,
@@ -31,33 +34,67 @@ end
3134

3235
conversion_trait(x::Type{<:Violin}) = SampleBased()
3336

34-
function plot!(plot::Violin)
35-
x, y, width, side, show_median = plot[1], plot[2], plot[:width], plot[:side], plot[:show_median]
36-
npoints, boundary, bandwidth = plot[:npoints], plot[:boundary], plot[:bandwidth]
37-
dodge, n_dodge, x_gap, dodge_gap = plot[:dodge], plot[:n_dodge], plot[:x_gap], plot[:dodge_gap]
37+
getuniquevalue(v, idxs) = v
38+
39+
function getuniquevalue(v::AbstractVector, idxs)
40+
u = view(v, idxs)
41+
f = first(u)
42+
msg = "Collection must have the same value across all indices"
43+
all(isequal(f), u) || throw(ArgumentError(msg))
44+
return f
45+
end
3846

39-
signals = lift(x, y, width, dodge, n_dodge, x_gap, dodge_gap, side, show_median, npoints, boundary, bandwidth) do x, y, width, dodge, n_dodge, x_gap, dodge_gap, vside, show_median, n, bound, bw
47+
function plot!(plot::Violin)
48+
x, y = plot[1], plot[2]
49+
args = @extract plot (width, side, color, show_median, npoints, boundary, bandwidth,
50+
datalimits, max_density, dodge, n_dodge, x_gap, dodge_gap)
51+
signals = lift(x, y, args...) do x, y, width, vside, color, show_median, n, bound, bw, limits, max_density, dodge, n_dodge, x_gap, dodge_gap
4052
x̂, violinwidth = xw_from_dodge(x, width, 1, x_gap, dodge, n_dodge, dodge_gap)
41-
vertices = Vector{Point2f0}[]
42-
lines = Pair{Point2f0, Point2f0}[]
43-
for (key, idxs) in StructArrays.finduniquesorted(x̂)
53+
54+
# Allow `side` to be either scalar or vector
55+
sides = broadcast(x̂, vside) do _, s
56+
return s == :left ? - 1 : s == :right ? 1 : 0
57+
end
58+
59+
sa = StructArray((x = x̂, side = sides))
60+
61+
specs = map(StructArrays.finduniquesorted(sa)) do (key, idxs)
4462
v = view(y, idxs)
4563
k = KernelDensity.kde(v;
4664
npoints = n,
4765
(bound === automatic ? NamedTuple() : (boundary = bound,))...,
4866
(bw === automatic ? NamedTuple() : (bandwidth = bw,))...,
4967
)
50-
spec = (x = key, kde = k, median = median(v))
51-
min, max = extrema_nan(spec.kde.density)
68+
l1, l2 = limits isa Function ? limits(v) : limits
69+
i1, i2 = searchsortedfirst(k.x, l1), searchsortedlast(k.x, l2)
70+
kde = (x = view(k.x, i1:i2), density = view(k.density, i1:i2))
71+
c = getuniquevalue(color, idxs)
72+
return (x = key.x, side = key.side, color = to_color(c), kde = kde, median = median(v))
73+
end
74+
75+
max = if max_density === automatic
76+
maximum(specs) do spec
77+
_, max = extrema_nan(spec.kde.density)
78+
return max
79+
end
80+
else
81+
max_density
82+
end
83+
84+
vertices = Vector{Point2f0}[]
85+
lines = Pair{Point2f0, Point2f0}[]
86+
colors = RGBA{Float32}[]
87+
88+
for spec in specs
5289
scale = 0.5*violinwidth/max
5390
xl = reverse(spec.x .- spec.kde.density .* scale)
5491
xr = spec.x .+ spec.kde.density .* scale
5592
yl = reverse(spec.kde.x)
5693
yr = spec.kde.x
5794

58-
x_coord, y_coord = if vside == :left
95+
x_coord, y_coord = if spec.side == -1 # left violin
5996
[spec.x; xl; spec.x], [yl[1]; yl; yl[end]]
60-
elseif vside == :right
97+
elseif spec.side == 1 # right violin
6198
[spec.x; xr; spec.x], [yr[1]; yr; yr[end]]
6299
else
63100
[spec.x; xr; spec.x; xl], [yr[1]; yr; yl[1]; yl]
@@ -72,22 +109,30 @@ function plot!(plot::Violin)
72109
ym₋, ym₊ = spec.kde.density[ip-1], spec.kde.density[ip]
73110
xm₋, xm₊ = spec.kde.x[ip-1], spec.kde.x[ip]
74111
ym = (xm * (ym₊ - ym₋) + xm₊ * ym₋ - xm₋ * ym₊) / (xm₊ - xm₋)
75-
median_left = Point2f0(vside == :right ? spec.x : spec.x - ym * scale, xm)
76-
median_right = Point2f0(vside == :left ? spec.x : spec.x + ym * scale, xm)
112+
median_left = Point2f0(spec.side == 1 ? spec.x : spec.x - ym * scale, xm)
113+
median_right = Point2f0(spec.side == -1 ? spec.x : spec.x + ym * scale, xm)
77114
push!(lines, median_left => median_right)
78115
end
116+
117+
push!(colors, spec.color)
79118
end
80-
return vertices, lines
119+
120+
return (vertices = vertices, lines = lines, colors = colors)
81121
end
82-
t = copy(Theme(plot))
83-
mediancolor = pop!(t, :mediancolor)
84-
poly!(plot, t, lift(first, signals))
122+
123+
poly!(
124+
plot,
125+
lift(s -> s.vertices, signals),
126+
color = lift(s -> s.colors, signals),
127+
strokecolor = plot[:strokecolor],
128+
strokewidth = plot[:strokewidth],
129+
)
85130
linesegments!(
86131
plot,
87-
lift(last, signals),
132+
lift(s -> s.lines, signals),
88133
color = lift(
89134
(mc, sc) -> mc === automatic ? sc : mc,
90-
mediancolor,
135+
plot[:mediancolor],
91136
plot[:strokecolor],
92137
),
93138
linewidth = plot[:medianlinewidth],

0 commit comments

Comments
 (0)