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

hexbin weights #3074

Merged
merged 12 commits into from
Jul 19, 2023
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## master

- `hexbin` now supports any per-observation weights which StatsBase respects - `<: StatsBase.AbstractWeights`, `Vector{Real}`, or `nothing` (the default). [#2804](https://github.com/MakieOrg/Makie.jl/pulls/2804)
- Added a new Axis type, `PolarAxis`, which is an axis with a polar projection. Input is in `(r, theta)` coordinates and is transformed to `(x, y)` coordinates using the standard polar-to-cartesian transformation.
Generally, its attributes are very similar to the usual `Axis` attributes, but `x` is replaced by `r` and `y` by `θ`.
It also inherits from the theme of `Axis` in this manner, so should work seamlessly with Makie themes [#2990](https://github.com/MakieOrg/Makie.jl/pull/2990).
Expand Down
24 changes: 12 additions & 12 deletions ReferenceTests/src/tests/examples2d.jl
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ end

@reference_test "Streamplot animation" begin
v(x::Point2{T}, t) where T = Point2{T}(one(T) * x[2] * t, 4 * x[1])
sf = Observable(Base.Fix2(v, 0e0))
sf = Observable(Base.Fix2(v, 0.0))
title_str = Observable("t = 0.00")
sp = streamplot(sf, -2..2, -2..2;
linewidth=2, colormap=:magma, axis=(;title=title_str))
Expand Down Expand Up @@ -692,7 +692,7 @@ end
y = [sin.(angles); 2 .* sin.(angles .+ pi/n)]
z = (x .- 0.5).^2 + (y .- 0.5).^2 .+ 0.5.* RNG.randn.()

inner = [n:-1:1; n] # clockwise inner
inner = [n:-1:1; n] # clockwise inner
outer = [(n+1):(2n); n+1] # counter-clockwise outer
boundary_nodes = [[outer], [inner]]
tri = DelaunayTriangulation.triangulate([x'; y'], boundary_nodes = boundary_nodes)
Expand All @@ -707,16 +707,16 @@ end
[(25.0, 0.0), (25.0, 5.0), (25.0, 10.0), (25.0, 15.0), (25.0, 20.0), (25.0, 25.0)],
[(25.0, 25.0), (20.0, 25.0), (15.0, 25.0), (10.0, 25.0), (5.0, 25.0), (0.0, 25.0)],
[(0.0, 25.0), (0.0, 20.0), (0.0, 15.0), (0.0, 10.0), (0.0, 5.0), (0.0, 0.0)]
]
]
curve_2 = [
[(4.0, 6.0), (4.0, 14.0), (4.0, 20.0), (18.0, 20.0), (20.0, 20.0)],
[(20.0, 20.0), (20.0, 16.0), (20.0, 12.0), (20.0, 8.0), (20.0, 4.0)],
[(20.0, 4.0), (16.0, 4.0), (12.0, 4.0), (8.0, 4.0), (4.0, 4.0), (4.0, 6.0)]
]
]
curve_3 = [
[(12.906, 10.912), (16.0, 12.0), (16.16, 14.46), (16.29, 17.06),
(13.13, 16.86), (8.92, 16.4), (8.8, 10.9), (12.906, 10.912)]
]
]
curves = [curve_1, curve_2, curve_3]
points = [
(3.0, 23.0), (9.0, 24.0), (9.2, 22.0), (14.8, 22.8), (16.0, 22.0),
Expand Down Expand Up @@ -989,24 +989,24 @@ end
f = Figure()
hist(
f[1, 1],
RNG.randn(10^6);
RNG.randn(10^6);
axis=(; yscale=log2)
)
hist(
f[1, 2],
RNG.randn(10^6);
RNG.randn(10^6);
axis=(; xscale=log2),
direction = :x
)
# make a gap in histogram as edge case
hist(
f[2, 1],
filter!(x-> x<0 || x > 1.5, RNG.randn(10^6));
filter!(x-> x<0 || x > 1.5, RNG.randn(10^6));
axis=(; yscale=log10)
)
hist(
f[2, 2],
filter!(x-> x<0 || x > 1.5, RNG.randn(10^6));
filter!(x-> x<0 || x > 1.5, RNG.randn(10^6));
axis=(; xscale=log10),
direction = :x
)
Expand Down Expand Up @@ -1049,17 +1049,17 @@ end
end

@reference_test "Z-translation within a recipe" begin
# This is testing whether backends respect the
# This is testing whether backends respect the
# z-level of plots within recipes in 2d.
# Ideally, the output of this test
# would be a blue line with red scatter markers.
# However, if a backend does not correctly pick up on translations,
# then this will be drawn in the drawing order, and blue
# will completely obscure red.

# It seems like we can't define recipes in `@reference_test` yet,
# so we'll have to fake a recipe's structure.

fig = Figure(resolution = (600, 600))
# Create a recipe plot
ax, plot_top = heatmap(fig[1, 1], randn(10, 10))
Expand Down
31 changes: 31 additions & 0 deletions docs/examples/plotting_functions/hexbin.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,34 @@ Colorbar(f[1, 2], hb,
f
```
\end{examplefigure}

### Applying weights to observations

\begin{examplefigure}{svg = true}
```julia
using CairoMakie
using CairoMakie.Makie # hide
using CairoMakie.Makie.StatsBase # hide
CairoMakie.activate!() # hide

using Random
Random.seed!(1234)

f = Figure(resolution = (800, 800))

x = 1:100
y = 1:100
points = vec(Point2f.(x, y'))

weights = [nothing, rand(length(points)), Makie.StatsBase.eweights(length(points), 0.005), Makie.StatsBase.weights(randn(length(points)))]
weight_labels = ["No weights", "Vector{<: Real}", "Exponential weights (StatsBase.eweights)", "StatesBase.weights(randn(...))"]

for (i, (weight, title)) in enumerate(zip(weights, weight_labels))
ax = Axis(f[fldmod1(i, 2)...], title = title, aspect = DataAspect())
hexbin!(ax, points; weights = weight)
autolimits!(ax)
end

f
```
\end{examplefigure}
27 changes: 18 additions & 9 deletions src/stats/hexbin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Plots a heatmap with hexagonal bins for the observations `xs` and `ys`.

### Specific to `Hexbin`

- `weights = nothing`: Weights for each observation. Can be `nothing` (each observation carries weight 1) or any `AbstractVector{<: Real}` or `StatsBase.AbstractWeights`.
- `bins = 20`: If an `Int`, sets the number of bins in x and y direction. If a `Tuple{Int, Int}`, sets the number of bins for x and y separately.
- `cellsize = nothing`: If a `Real`, makes equally-sided hexagons with width `cellsize`. If a `Tuple{Real, Real}` specifies hexagon width and height separately.
- `threshold::Int = 1`: The minimal number of observations in the bin to be shown. If 0, all zero-count hexagons fitting into the data limits will be shown.
Expand All @@ -21,12 +22,14 @@ Plots a heatmap with hexagonal bins for the observations `xs` and `ys`.
return Attributes(;
colormap=theme(scene, :colormap),
colorrange=Makie.automatic,
weights=nothing,
bins=20,
cellsize=nothing,
threshold=1,
scale=identity,
strokewidth=0,
strokecolor=:black)
strokecolor=:black,
)
end

function spacings_offsets_nbins(bins::Tuple{Int,Int}, cellsize::Nothing, xmi, xma, ymi, yma)
Expand Down Expand Up @@ -69,14 +72,18 @@ function data_limits(hb::Hexbin)
return Rect3f(no, nw)
end

get_weight(weights, i) = Float64(weights[i])
get_weight(::StatsBase.UnitWeights, i) = 1e0
get_weight(::Nothing, i) = 1e0

function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
xy = hb[1]

points = Observable(Point2f[])
count_hex = Observable(Float64[])
markersize = Observable(Vec2f(1, 1))

function calculate_grid(xy, bins, cellsize, threshold, scale)
function calculate_grid(xy, weights, bins, cellsize, threshold, scale)
empty!(points[])
empty!(count_hex[])

Expand All @@ -94,21 +101,22 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
y_diff = yma - ymi

xspacing, yspacing, xoff, yoff, nbinsx, nbinsy = spacings_offsets_nbins(bins, cellsize, xmi, xma, ymi,
yma)
yma)

ysize = yspacing / 3 * 4
ry = ysize / 2

xsize = xspacing * 2
rx = xsize / sqrt3

d = Dict{Tuple{Int,Int},Int}()
d = Dict{Tuple{Int,Int}, Float64}()

# for the distance measurement, the y dimension must be weighted relative to the x
# dimension according to the different sizes in each, otherwise the attribution to hexagonal
# cells is wrong
yweight = xsize / ysize

i = 1
for (_x, _y) in xy
nx, nxs, dvx = nearest_center(_x, xspacing, xoff)
ny, nys, dvy = nearest_center(_y, yspacing, yoff)
Expand All @@ -119,7 +127,7 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
is_grid1 = d1 < d2

# _xy = is_grid1 ? (nx, ny) : (nxs, nys)

id = if is_grid1
(
cld(dvx, 2),
Expand All @@ -132,7 +140,8 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
)
end

d[id] = get(d, id, 0) + 1
d[id] = get(d, id, 0) + (get_weight(weights, i))
i += 1
end

if threshold == 0
Expand All @@ -141,7 +150,7 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
for ix in 0:_nx-1
_x = xoff + 2 * ix * xspacing + (isodd(iy) * xspacing)
_y = yoff + iy * yspacing
c = get(d, (ix, iy), 0)
c = get(d, (ix, iy), 0.0)
push!(points[], Point2f(_x, _y))
push!(count_hex[], scale(c))
end
Expand All @@ -162,7 +171,7 @@ function Makie.plot!(hb::Hexbin{<:Tuple{<:AbstractVector{<:Point2}}})
notify(points)
return notify(count_hex)
end
onany(calculate_grid, xy, hb.bins, hb.cellsize, hb.threshold, hb.scale)
onany(calculate_grid, xy, hb.weights, hb.bins, hb.cellsize, hb.threshold, hb.scale)
# trigger once
notify(hb.bins)

Expand Down Expand Up @@ -212,4 +221,4 @@ function nearest_center(val, spacing, offset)
rounded = offset + spacing * (dv + isodd(dv))
rounded_scaled = offset + spacing * (dv + iseven(dv))
return rounded, rounded_scaled, dv
end
end
Loading