Skip to content

Commit

Permalink
Apply the transform_func on positions, to work in nonlinear scale (#14)
Browse files Browse the repository at this point in the history
* Apply the transform_func as well in order to work in nonlinear scale

* Fix typos

* Transform back until we have `space == :transformed`

* Add an example of the Julia microbenchmarks in beeswarm

* scale -> scales.md

* dict not dataframe in example

* Finally fix example

* Even more unconventional example

* fix Octave

* Fix color type

* Better file structure for examples

* clean up

* Do not edit marker position if group is empty

This fixes issues encountered when using missing or NaN data.
  • Loading branch information
asinghvi17 authored Apr 20, 2024
1 parent d4c1056 commit a0e233b
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 7 deletions.
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

0 comments on commit a0e233b

Please sign in to comment.