diff --git a/.gitignore b/.gitignore index cf05e1a..3dd4275 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ */node_modules/* .DS_Store -docs/src/examples.md \ No newline at end of file +docs/src/examples/examples.md \ No newline at end of file diff --git a/docs/Project.toml b/docs/Project.toml index 62d700a..2ec3145 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,10 +1,14 @@ [deps] AlgebraOfGraphics = "cbdf2221-f076-402e-a563-3d30da359d67" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" PalmerPenguins = "8b842266-38fa-440a-9b57-31493939ab85" RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" +Rsvg = "c4c386cf-5103-5370-be45-f3a111cca3b8" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" SwarmMakie = "0b1c068e-6a84-4e66-8136-5c95cafa83ed" diff --git a/docs/make.jl b/docs/make.jl index 08f9ba5..9130180 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -65,7 +65,7 @@ withenv("JULIA_DEBUG" => "Literate") do # allow Literate debug output to escape end # As a special case, literatify the examples.jl file in docs/src to Documenter markdown -Literate.markdown(joinpath(@__DIR__, "src", "examples.jl"), joinpath(@__DIR__, "src"); flavor = Literate.DocumenterFlavor(), postprocess = _replace_example_with_figure) +Literate.markdown(joinpath(@__DIR__, "src", "examples", "examples.jl"), joinpath(@__DIR__, "src", "examples"); flavor = Literate.DocumenterFlavor(), postprocess = _replace_example_with_figure) makedocs(; modules=[SwarmMakie], @@ -80,7 +80,11 @@ makedocs(; "Introduction" => "introduction.md", "Algorithms" => "algorithms.md", "Gutters" => "gutters.md", - "Examples" => "examples.md", + "Examples" => [ + "examples/examples.md", + "Nonlinear scales" => "examples/scales.md", + "Unconventional use" => "examples/unconventional.md" + ], "API Reference" => "api.md", "Source code" => literate_pages, ], diff --git a/docs/src/examples.jl b/docs/src/examples/examples.jl similarity index 100% rename from docs/src/examples.jl rename to docs/src/examples/examples.jl diff --git a/docs/src/examples/scales.md b/docs/src/examples/scales.md new file mode 100644 index 0000000..db759b5 --- /dev/null +++ b/docs/src/examples/scales.md @@ -0,0 +1,3 @@ +# Scaled beeswarm plots + +Beeswarm plots can also be plotted in log-scale axes! \ No newline at end of file diff --git a/docs/src/examples/unconventional.md b/docs/src/examples/unconventional.md new file mode 100644 index 0000000..5be126e --- /dev/null +++ b/docs/src/examples/unconventional.md @@ -0,0 +1,212 @@ +# Unconventional swarm plots + +You can use swarm plots to simply separate scatter markers which share the same `x` coordinate, and distinguish them by color and marker type. + + +## The Julia benchmark plot + +```@example julia-benchmark +# Load the required Julia packages +using Base.MathConstants +using CSV +using DataFrames +using AlgebraOfGraphics, SwarmMakie, CairoMakie +using StatsBase, CategoricalArrays + +# Load benchmark data from file +benchmarks = + CSV.read(download("https://raw.githubusercontent.com/JuliaLang/Microbenchmarks/master/bin/benchmarks.csv"), DataFrame; header = ["language", "benchmark", "time"]) + +# Capitalize and decorate language names from datafile +dict = Dict( + "c" => "C", + "julia" => "Julia", + "lua" => "LuaJIT", + "fortran" => "Fortran", + "java" => "Java", + "javascript" => "JavaScript", + "matlab" => "Matlab", + "mathematica" => "Mathematica", + "python" => "Python", + "octave" => "Octave", + "r" => "R", + "rust" => "Rust", + "go" => "Go", +); +benchmarks[!, :language] = [dict[lang] for lang in benchmarks[!, :language]] + +# Normalize benchmark times by C times +ctime = benchmarks[benchmarks[!, :language] .== "C", :] +benchmarks = innerjoin(benchmarks, ctime, on = :benchmark, makeunique = true) +select!(benchmarks, Not(:language_1)) +rename!(benchmarks, :time_1 => :ctime) +benchmarks[!, :normtime] = benchmarks[!, :time] ./ benchmarks[!, :ctime]; + +# Compute the geometric mean for each language +langs = []; +means = []; +priorities = []; +for lang in benchmarks[!, :language] + data = benchmarks[benchmarks[!, :language] .== lang, :] + gmean = geomean(data[!, :normtime]) + push!(langs, lang) + push!(means, gmean) + if (lang == "C") + push!(priorities, 1) + elseif (lang == "Julia") + push!(priorities, 2) + else + push!(priorities, 3) + end +end + +# Add the geometric means back into the benchmarks dataframe +langmean = Dict(langs .=> tuple.(means, priorities)) +benchmarks.geomean = first.(getindex.((langmean,), benchmarks.language)) +benchmarks.priority = last.(getindex.((langmean,), benchmarks.language)) + +# Put C first, Julia second, and sort the rest by geometric mean +sort!(benchmarks, [:priority, :geomean]); + +langs = CategoricalArray(benchmarks.language) +bms = CategoricalArray(benchmarks.benchmark) + +f, a, p = beeswarm( + langs.refs, benchmarks.normtime; + color = bms.refs, + colormap = :Set2_8, + # markersize = 5, + marker = Circle, + axis = (; + yscale = log10, + xticklabelrotation = 0, + xticklabelsize = 12, + xticksvisible = false, + topspinecolor = :gray, + bottomspinecolor = :gray, + leftspinecolor = :gray, + rightspinecolor = :gray, + ylabel = "Time relative to C", + xticks = (1:length(unique(langs)), langs.pool.levels), + xminorticks = IntervalsBetween(2), + xgridvisible = false, + xminorgridvisible = true, + xminorgridcolor = (:black, 0.2), + yminorticks = IntervalsBetween(5), + yminorgridvisible = true, + ), + figure = (; size = (1000, 618),) +) +leg = Legend(f[1, 2], + [MarkerElement(; color = Makie.categorical_colors(:Set1_8, 8)[i], marker = :circle, markersize = 11) for i in 1:length(bms.pool.levels)], + bms.pool.levels, + "Benchmark"; +) +f + +``` + + +## Benchmarks colored by language + +```@example julia-benchmark + +f, a, p = beeswarm( + bms.refs, benchmarks.normtime; + color = langs.refs, + colormap = Makie.Colors.distinguishable_colors(13),#:Set1_9, + # markersize = 5, + marker = Circle, + axis = (; + yscale = log10, + xticklabelrotation = 0, + xticklabelsize = 12, + xticksvisible = false, + topspinecolor = :gray, + bottomspinecolor = :gray, + leftspinecolor = :gray, + rightspinecolor = :gray, + ylabel = "Time relative to C", + xticks = (1:length(unique(bms)), bms.pool.levels), + xminorticks = IntervalsBetween(2), + xgridvisible = false, + xminorgridvisible = true, + xminorgridcolor = (:black, 0.2), + yminorticks = IntervalsBetween(5), + yminorgridvisible = true, + ), + figure = (; size = (1000, 618),) +) +leg = Legend(f[1, 2], + [MarkerElement(; color = p.colormap[][i], marker = :circle, markersize = 11) for i in 1:length(langs.pool.levels)], + langs.pool.levels, + "Benchmark"; +) +f +``` + +## Custom markers +```@example julia-benchmark +# Get logos for programming languages + +using Rsvg +using CairoMakie +using CairoMakie.Cairo, CairoMakie.FileIO + +function pngify(input_data::AbstractString) + r = Rsvg.handle_new_from_data(String(input_data)); + Rsvg.handle_set_dpi(r, 2.0) + d = Rsvg.handle_get_dimensions(r); + img = fill(Makie.Colors.ARGB32(0, 0, 0, 0), d.width * 4, d.height * 4) + # create an image surface to draw onto the image + surf = Cairo.CairoImageSurface(img) + ctx = Cairo.CairoContext(surf); + Cairo.scale(ctx, 4, 4) + Rsvg.handle_render_cairo(ctx,r); + return permutedims(img) +end + + +language_logo_url(lang::String) = "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/$(lowercase(lang))/$(lowercase(lang))-original.svg" + +language_marker_dict = Dict( + [key => read(download(language_logo_url(key)), String) |> pngify for key in ("c", "fortran", "go", "java", "javascript", "julia", "matlab", "python", "r", "rust")] +) + +language_marker_dict["octave"] = FileIO.load(File{format"PNG"}(download("https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Gnu-octave-logo.svg/2048px-Gnu-octave-logo.svg.png"))) .|> Makie.Colors.ARGB32 + +language_marker_dict["luajit"] = read(download(language_logo_url("lua")), String) |> pngify +language_marker_dict["mathematica"] = read(download("https://upload.wikimedia.org/wikipedia/commons/2/20/Mathematica_Logo.svg"), String) |> pngify + + +f, a, p = beeswarm( + bms.refs, benchmarks.normtime; + marker = getindex.((language_marker_dict,),lowercase.(benchmarks.language)), + markersize = 11, + axis = (; + yscale = log10, + xticklabelrotation = 0, + xticklabelsize = 12, + xticksvisible = false, + topspinecolor = :gray, + bottomspinecolor = :gray, + leftspinecolor = :gray, + rightspinecolor = :gray, + ylabel = "Time relative to C", + xticks = (1:length(unique(bms)), bms.pool.levels), + xminorticks = IntervalsBetween(2), + xgridvisible = false, + xminorgridvisible = true, + xminorgridcolor = (:black, 0.2), + yminorticks = IntervalsBetween(5), + yminorgridvisible = true, + ), + figure = (; size = (1000, 618),) +) +leg = Legend(f[1, 2], + [MarkerElement(; marker = language_marker_dict[lowercase(lang)], markersize = 15) for lang in langs.pool.levels], + langs.pool.levels, + "Language"; +) +f +``` \ No newline at end of file diff --git a/src/algorithms/simple.jl b/src/algorithms/simple.jl index 0e9f0a4..93e6a69 100644 --- a/src/algorithms/simple.jl +++ b/src/algorithms/simple.jl @@ -27,7 +27,12 @@ function calculate!(buffer::AbstractVector{<: Point2}, alg::SimpleBeeswarm, posi for x_val in unique(xs) group = findall(==(x_val), xs) - xs[group] .= simple_xs(view(ys, group), markersize, side) + view_ys = view(ys, group) + if isempty(view_ys) + continue + else + xs[group] .= simple_xs(view_ys, markersize, side) + end end buffer .= Point2f.(xs .+ first.(positions), last.(positions)) diff --git a/src/recipe.jl b/src/recipe.jl index 680e35a..c5ecb19 100644 --- a/src/recipe.jl +++ b/src/recipe.jl @@ -99,7 +99,7 @@ function Makie.plot!(plot::Beeswarm) point_buffer = Observable{Vector{Point2f}}(zeros(Point2f, length(positions[]))) pixelspace_point_buffer = Observable{Vector{Point2f}}(zeros(Point2f, length(positions[]))) # when the positions change, we must update the buffer arrays - onany(plot, plot.converted[1], plot.algorithm, plot.color, plot.markersize, plot.side, plot.direction, plot.gutter, plot.gutter_threshold, should_update_based_on_zoom) do positions, algorithm, colors, markersize, side, direction, gutter, gutter_threshold, _ + onany(plot, plot.converted[1], plot.algorithm, plot.transformation.transform_func, plot.markersize, plot.side, plot.direction, plot.gutter, plot.gutter_threshold, should_update_based_on_zoom) do positions, algorithm, tfunc, markersize, side, direction, gutter, gutter_threshold, _ @assert side in (:both, :left, :right) "side should be one of :both, :left, or :right, got $(side)" @assert direction in (:x, :y) "direction should be one of :x or :y, got $(direction)" if length(positions) != length(point_buffer[]) @@ -107,12 +107,17 @@ function Makie.plot!(plot::Beeswarm) point_buffer.val = copy(positions) pixelspace_point_buffer.val = zeros(Point2f, length(positions)) end + # Apply nonlinear transform function if any + pixelspace_point_buffer.val .= Makie.apply_transform(tfunc, positions, :data) # Project input positions from data space to pixel space - pixelspace_point_buffer.val .= Point2f.(Makie.project.((scene.camera,), :data, :pixel, direction == :y ? positions : reverse.(positions))) + pixelspace_point_buffer.val .= Point2f.(Makie.project.((scene.camera,), :data, :pixel, direction == :y ? pixelspace_point_buffer.val : reverse.(pixelspace_point_buffer.val))) # Calculate the beeswarm in pixel space and store it in `point_buffer.val` calculate!(point_buffer.val, algorithm, direction == :y ? pixelspace_point_buffer.val : reverse.(pixelspace_point_buffer.val), markersize, side) # Project the beeswarm back to data space and store it, again, in `point_buffer.val` point_buffer.val .= Point2f.(Makie.project.((scene.camera,), :pixel, :data, direction == :y ? (point_buffer.val) : reverse.(point_buffer.val))) + # Finally, apply the inverse transform to move back into data space. + # TODO: remove this once we have `space==:transformed` in Makie. + point_buffer.val .= Makie.apply_transform(Makie.inverse_transform(tfunc), point_buffer.val, :data) # Method to create a gutter when a gutter is defined # NOTE: Maybe turn this into a helper function? @@ -187,4 +192,4 @@ function gutterize!(point_buffer, algorithm::BeeswarmAlgorithm, positions, direc """ end end -end \ No newline at end of file +end