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

Apply the transform_func as well in order to work in nonlinear scale #14

Merged
merged 14 commits into from
Apr 20, 2024
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
*/node_modules/*
.DS_Store

docs/src/examples.md
docs/src/examples/examples.md
4 changes: 4 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 6 additions & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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,
],
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions docs/src/examples/scales.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Scaled beeswarm plots

Beeswarm plots can also be plotted in log-scale axes!
212 changes: 212 additions & 0 deletions docs/src/examples/unconventional.md
Original file line number Diff line number Diff line change
@@ -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
```
7 changes: 6 additions & 1 deletion src/algorithms/simple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
11 changes: 8 additions & 3 deletions src/recipe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,25 @@ 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[])
# recreate the point buffers if lengths have changed
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?
Expand Down Expand Up @@ -187,4 +192,4 @@ function gutterize!(point_buffer, algorithm::BeeswarmAlgorithm, positions, direc
"""
end
end
end
end
Loading