From 5ee7025de4a9ab10c94fe67e1025571cb994974d Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 4 Mar 2024 18:57:50 -0500 Subject: [PATCH] Add real content --- docs/package.json | 18 ++++ src/SwarmMakie.jl | 7 +- .../mkborregaard.jl} | 23 ++--- src/algorithms/simple.jl | 13 +++ src/recipe.jl | 96 +++++++++++++++++++ 5 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 docs/package.json rename src/{beeswarm.jl => algorithms/mkborregaard.jl} (86%) create mode 100644 src/algorithms/simple.jl create mode 100644 src/recipe.jl diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..71458cb --- /dev/null +++ b/docs/package.json @@ -0,0 +1,18 @@ +{ + "devDependencies": { + "markdown-it": "^14.0.0", + "markdown-it-mathjax3": "^4.3.2", + "vitepress": "^1.0.0-rc.43", + "vitepress-plugin-tabs": "^0.5.0", + "vitest": "^1.3.0" + }, + "scripts": { + "docs:dev": "vitepress dev build/.documenter", + "docs:build": "vitepress build build/.documenter", + "docs:preview": "vitepress preview build/.documenter" + }, + "dependencies": { + "@shikijs/transformers": "^1.1.7", + "markdown-it-footnote": "^4.0.0" + } +} diff --git a/src/SwarmMakie.jl b/src/SwarmMakie.jl index 15bec92..4a5d18c 100644 --- a/src/SwarmMakie.jl +++ b/src/SwarmMakie.jl @@ -1,5 +1,10 @@ +# # SwarmMakie.jl + module SwarmMakie -# Write your package code here. +using Makie, Random + +include("recipe.jl") +include("algorithms/simple.jl") end diff --git a/src/beeswarm.jl b/src/algorithms/mkborregaard.jl similarity index 86% rename from src/beeswarm.jl rename to src/algorithms/mkborregaard.jl index 922985d..c28cc0d 100644 --- a/src/beeswarm.jl +++ b/src/algorithms/mkborregaard.jl @@ -1,16 +1,3 @@ -using Random: shuffle -using Makie -@recipe(Beeswarm, positions) do scene - return default_theme(scene, Scatter) -end - -Makie.conversion_trait(::Type{<: Beeswarm}) = Makie.PointBased() - -function Makie.plot!(plot::Beeswarm) - positions = beeswarm.converted[1] # being PointBased, it should always receive a vector of Point2 - markersize = beeswarm.markersize - -end function beeswarm_coords(olda, side = :both) @@ -40,10 +27,10 @@ function beeswarm_coords(olda, side = :both) end function potential_interactions(freeind) - outside_range(x) = abs(a[freeind]-a[x])>cellsize - lo_ind = findprev(outside_range, LinearIndices(a), freeind) + outside_range(x) = abs(a[freeind] - x) > cellsize + lo_ind = findprev(outside_range, a, freeind) lo = isnothing(lo_ind) ? 1 : lo_ind + 1 - hi_ind = findnext(outside_range, LinearIndices(a), freeind) + hi_ind = findnext(outside_range, a, freeind) hi = isnothing(hi_ind) ? lastindex(a)-1 : hi_ind - 1 if lo != 0 && hi != 0 && hi >= lo included[freeind] ? (lo:hi)[.!(included[lo:hi])] : (lo:hi)[included[lo:hi]] @@ -64,9 +51,11 @@ function beeswarm_coords(olda, side = :both) nearest_ypos = fill(NaN, length(a)) while !isnothing(ind) included[ind] = true - ind = findfirst(v->v>a[ind] + cellsize, a) + ind = findfirst(v -> v > (a[ind] + cellsize), a) end + @show sum(included) + # now fill the rest in turn update_ypos!(nearest_ypos, findall(.!(included))) innow = sum(included) diff --git a/src/algorithms/simple.jl b/src/algorithms/simple.jl new file mode 100644 index 0000000..5444c28 --- /dev/null +++ b/src/algorithms/simple.jl @@ -0,0 +1,13 @@ +""" + SimpleBeeswarm() + +A simple implementation like Matplotlib's algorithm. +""" +struct SimpleBeeswarm <: BeeswarmAlgorithm +end + +function calculate!(buffer::AbstractVector{<: Point2}, alg::SimpleBeeswarm, positions::AbstractVector{<: Point2}, markersize) + + buffer .= positions + return +end diff --git a/src/recipe.jl b/src/recipe.jl new file mode 100644 index 0000000..45dda7f --- /dev/null +++ b/src/recipe.jl @@ -0,0 +1,96 @@ +# # Beeswarm recipe + +# In this file, we define the `Beeswarm` recipe. + +@recipe(Beeswarm, positions) do scene + return merge( + Attributes( + algorithm = NoBeeswarm(), + ), + default_theme(scene, Scatter), + ) +end + +Makie.conversion_trait(::Type{<: Beeswarm}) = Makie.PointBased() + +# this is subtyped by e.g. `SimpleBeeswarm` and `VerticallyChallengedBeeswarm` +abstract type BeeswarmAlgorithm end + +struct NoBeeswarm <: BeeswarmAlgorithm +end + +function calculate!(buffer::AbstractVector{<: Point2}, alg::NoBeeswarm, positions::AbstractVector{<: Point2}, markersize) + @info "Calculating..." + buffer .= positions + return +end + + +function Makie.plot!(plot::Beeswarm) + positions = plot.converted[1] # being PointBased, it should always receive a vector of Point2 + @assert positions[] isa AbstractVector{<: Point2} "`positions` should be an `AbstractVector` of `Point2` after conversion, got type $(typeof(positions)). If you have passed in `x, y, z` input, be aware that `beeswarm` only accepts 2-D input (`x, y`)." + + # this is a bit risky but #YOLO + # we extract the plot's parent Scene, from which we can extract + # the viewport, i.e., pixelspace! + scene = Makie.parent_scene(plot) + # Now, we can extract the Scene's limits from the camera's projectionview. + # Note that this only works for 2-D Scenes, and gets us the transformed space limits, + # so if you're trying to run this in a scene with a `transform_func`, that's something to + # be aware of. + finalwidths = lift(scene.camera.projection) do pv + xmin, xmax = minmax((((-1, 1) .- pv[1, 4]) ./ pv[1, 1])...) + ymin, ymax = minmax((((-1, 1) .- pv[2, 4]) ./ pv[2, 2])...) + return Makie.Vec2(xmax - xmin, ymax - ymin) + end + # and its viewport (in case the scene changes size) + pixel_widths = @lift widths($(scene.viewport)) + old_pixel_widths = Ref(pixel_widths[]) + old_finalwidths = Ref(finalwidths[]) + + should_update_based_on_zoom = Observable(nothing) + lift(plot, finalwidths, pixel_widths) do fw, pw # if we change more than 5%, recalculate. + if !all(isapprox.(fw, old_finalwidths[]; rtol = 0.05)) || !all(isapprox.(pw, old_pixel_widths[]; rtol = 0.05)) + old_pixel_widths[] = pw + old_finalwidths[] = fw + notify(should_update_based_on_zoom) + end + end + + + # set up buffers + point_buffer = Observable{Vector{Point2f}}(zeros(Point2f, length(positions[]))) + pixelspace_point_buffer = Observable{Vector{Point2f}}(zeros(Point2f, length(positions[]))) + color_buffer = Observable{Vector{RGBA{Float32}}}() + # when the positions change, we must update the buffer arrays + onany(plot, plot.converted[1], plot.algorithm, plot.color, plot.markersize, should_update_based_on_zoom) do positions, algorithm, colors, markersize, _ + if length(positions) != length(point_buffer[]) + # recreate point buffer if lengths have changed + point_buffer.val = zeros(Point2f, length(positions)) + pixelspace_point_buffer.val = zeros(Point2f, length(positions)) + # color_buffer.val = zeros(RGBA{Float32}, length(positions)) + end + pixelspace_point_buffer.val .= Makie.project.((scene.camera, ), :data, :pixel, positions) + calculate!(point_buffer.val, algorithm, pixelspace_point_buffer.val, markersize) + point_buffer.val .= Makie.project.((scene.camera,), :pixel, :data, pixelspace_point_buffer.val) + # color_buffer.val .= colors # TODO: figure out some way to make this better. + + # update the scatter plot + notify(point_buffer) + end + # create a set of Attributes that we can pass down + attrs = copy(plot.attributes) + pop!(attrs, :algorithm) + pop!(attrs, :space) + # attrs[:color] = color_buffer + attrs[:space] = :data + attrs[:markerspace] = :pixel + # create the scatter plot + scatter_plot = scatter!( + plot, + attrs, + point_buffer + ) + return +end +