Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

You wouldnt pirate a type #119

Merged
merged 2 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b"
Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
TidierCats = "79ddc9fe-4dbf-4a56-a832-df41fb326d23"
TidierData = "fe2206b3-d496-4ee9-a338-6a095c4ece80"

[compat]
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ TidierPlots.jl is a 100% Julia implementation of the R package [ggplot2](https:/

## Installation

For the "stable" version, access the Pkg interface by pressing `]` at the `julia>` prompt, then type `add TidierPlots`.
For the "stable" version (currently 0.7.8), access the Pkg interface by pressing `]` at the `julia>` prompt, then type `add TidierPlots`.

For the development version:
For the development version (currently 0.8.0):

```julia
using Pkg
Pkg.add(url="https://github.com/TidierOrg/TidierPlots.jl")
```

TidierPlots will also be installed automatically if you `add Tidier`.

## What functions does TidierPlots.jl support?

TidierPlots.jl currently supports the top-level function `ggplot()`, plus:
Expand All @@ -47,7 +49,7 @@ Geoms:
- `geom_path`, `geom_line`, and `geom_step`
- `geom_bar`, `geom_col`, and `geom_histogram`
- `geom_boxplot` and `geom_violin`
- `geom_contour` and `geom_tile`
- `geom_tile`
- `geom_density`
- `geom_text` and `geom_label`

Expand Down Expand Up @@ -85,7 +87,6 @@ The goal of this package is to allow you to write code that is as similar to ggp

- Option 1: `aes` function, julia-style columns, e.g. `aes(x = :x, y = :y)` or `aes(:x, :y)`
- Option 2: `@aes` (or `@es`) macro, aes as in ggplot, e.g. `@aes(x = x, y = y)` or `@aes(x, y)`
- Option 3 (Deprecated): `aes` function, column names as strings, e.g. `aes(x = "x", y = "y")` or `aes("x", "y")`

If you use Option 1, functions can be applied to columns with the `=>` operator to form a `Pair{Symbol, Function}`, similar to how `DataFrames.jl` functions work.

Expand Down Expand Up @@ -124,7 +125,7 @@ Sort your categorical variables in order of appearance with a single keyword rat
@count(manufacturer)
@arrange(n)
ggplot(xticklabelrotation = .5)
geom_col(aes(y = :n, x = cat_inorder(:manufacturer)))
geom_col(@es(y = n, x = cat_inorder(manufacturer)))
end
```
![](assets/in_order.png)
Expand Down Expand Up @@ -217,7 +218,7 @@ Add basic support for any Makie plot using `geom_template(name, required_aes, ma
geom_raincloud = geom_template("geom_raincloud", ["x", "y"], :RainClouds)

ggplot(penguins) +
geom_raincloud(aes(x = :species, y = :bill_depth_mm/10, color = :species), size = 4) +
geom_raincloud(@aes(x = species, y = bill_depth_mm/10, color = species), size = 4) +
scale_y_continuous(labels = "{:.1f} cm") +
labs(title = "Bill Depth by Species", x = "Species", y = "Bill Depth") +
theme_minimal()
Expand Down
165 changes: 96 additions & 69 deletions src/draw.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import Makie.SpecApi

function Makie.SpecApi.Axis(plot::GGPlot)
plot_list = Makie.PlotSpec[]
plot_list_by_facet = nothing
facet_names = nothing
facet_positions = nothing
plot_list = Dict{Tuple,Vector{Makie.PlotSpec}}()
facet_boxes = Dict()
facet_labels = Dict()
axis_options = Dict{Symbol,Any}()
Expand Down Expand Up @@ -50,8 +47,27 @@ function Makie.SpecApi.Axis(plot::GGPlot)
push!(aes_df_list, select(plot_data, rule[1] => rule[2] => aes))
end

if !isnothing(plot.facet_options)
push!(aes_df_list,
DataFrame(facet = plot_data[!, plot.facet_options.wrap]))
end

aes_df = hcat(aes_df_list...)

# if there are no grouping aesthetics and no manually specified group, everything is part of the same group. Otherwise, make a grouping column out of the grouping aesthetics if one doesn't already exist
grouping_aes = intersect(
geom.grouping_aes,
Symbol.(names(aes_df))
)

if !("group" in names(aes_df))
if length(grouping_aes) == 0
aes_df.group .= 1
elseif !("group" in names(aes_df))
aes_df.group = string.([aes_df[!, col] for col in grouping_aes]...)
end
end

args_dict_makie = Dict{Symbol,Any}()

supported_kwargs = get(_accepted_options_by_type, geom.visual,
Expand Down Expand Up @@ -104,57 +120,80 @@ function Makie.SpecApi.Axis(plot::GGPlot)
ymax = max(ymax, maximum(aes_df.y))
end

grouping = length(intersect(
names(aes_df), geom.grouping_aes)) == 0

facets = !isnothing(plot.facet_options)
# if there are no facet options just plot everything in 1,1
# if there are, make a column that indicates which facet each point belongs to
if isnothing(plot.facet_options)
aes_df.facet .= [(1, 1)] # everything goes in the same "facet"
else
facet_names = unique(aes_df.facet)
facet_positions, facet_labels, facet_boxes =
position_facets(facet_names,
plot.facet_options.nrow,
plot.facet_options.ncol)
aes_df.facet = [facet_positions[k] for k in aes_df.facet]
end

# if there are no grouping_aes given and no facets required, we only need one PlotSpec
required_aes_data = []
for (key, group_aes_df) in pairs(groupby(aes_df, [:group, :facet]))
required_aes_data = []

for a in required_aes
data = aes_df[!, Symbol(a)]
if eltype(data) <: Union{AbstractString,CategoricalValue}
for a in required_aes
data = group_aes_df[!, Symbol(a)]
if !(Symbol(a) in _verbatim_aes)
labels = levels(CategoricalArray(data))
data = levelcode.(CategoricalArray(data))
axis_options[Symbol(a * "ticks")] = (
1:maximum(data),
string.(labels)
)
if eltype(data) <: AbstractString
labels = levels(CategoricalArray(data))
data = levelcode.(CategoricalArray(data))
axis_options[Symbol(a * "ticks")] = (
1:maximum(data),
string.(labels)
)
elseif eltype(data) <: CategoricalValue
labels = levels(data)
data = levelcode.(data)
axis_options[Symbol(a * "ticks")] = (
1:maximum(data),
string.(labels)
)
end
end
push!(required_aes_data, data)
end
push!(required_aes_data, data)
end

optional_aes_data = Dict()
optional_aes_data = Dict()

for a in names(aes_df)
if String(a) in required_aes
continue
end
if !isnothing(supported_kwargs)
if !(Symbol(a) in supported_kwargs)
for a in names(group_aes_df)
if String(a) in required_aes
continue
end
end
if !isnothing(supported_kwargs)
if !(Symbol(a) in supported_kwargs)
continue
end
end

data = aes_df[!, Symbol(a)]
data = group_aes_df[!, Symbol(a)]

if eltype(data) <: Union{AbstractString,RGB{FixedPoint}}
if !(Symbol(a) in _verbatim_aes)
data = Categorical(data)
if eltype(data) <: Union{AbstractString,RGB{FixedPoint}}
if !(Symbol(a) in _verbatim_aes)
data = Categorical(data)
end
end
end

push!(optional_aes_data, Symbol(a) => data)
end
push!(optional_aes_data, Symbol(a) => data)
end

args = Tuple([geom.visual, required_aes_data...])
kwargs = merge(args_dict_makie, optional_aes_data)
args = Tuple([geom.visual, required_aes_data...])
kwargs = merge(args_dict_makie, optional_aes_data)

# push completed PlotSpec (type, args, and kwargs) to the list of plots
push!(plot_list, Makie.PlotSpec(args...; kwargs...))
# push completed PlotSpec (type, args, and kwargs) to the list of plots in the appropriate facet
facet_position = key[2]
if haskey(plot_list, facet_position) # add to list if exists
plot_list[facet_position] = vcat(
plot_list[facet_position],
Makie.PlotSpec(args...; kwargs...))
else
plot_list[facet_position] = [Makie.PlotSpec(args...; kwargs...)]
end
end
end

# rename and correct types on all axis options
Expand All @@ -167,41 +206,29 @@ function Makie.SpecApi.Axis(plot::GGPlot)
end
end

if isnothing(plot.facet_options)
return length(axis_options) == 0 ?
Makie.SpecApi.Axis(plots=plot_list) :
Makie.SpecApi.Axis(plots=plot_list; axis_options...)
else
if !haskey(axis_options, :limits)
expand_x = (xmax - xmin) * 0.05
expand_y = (ymax - ymin) * 0.05

if !plot.facet_options.free_x && plot.facet_options.free_y
expandx = (xmax - xmin) * 0.05
axis_options[:limits] = ((xmin - expand_x, xmax + expand_x), nothing)
elseif plot.facet_options.free_x && !plot.facet_options.free_y
if !haskey(axis_options, :limits) && !isnothing(plot.facet_options)
expand_x = (xmax - xmin) * 0.05
expand_y = (ymax - ymin) * 0.05

if !plot.facet_options.free_x && plot.facet_options.free_y
expandx = (xmax - xmin) * 0.05
axis_options[:limits] = ((xmin - expand_x, xmax + expand_x), nothing)
elseif plot.facet_options.free_x && !plot.facet_options.free_y
axis_options[:limits] = (nothing, (ymin - expand_y, ymax + expand_y))
elseif !plot.facet_options.free_x && !plot.facet_options.free_y
elseif !plot.facet_options.free_x && !plot.facet_options.free_y
axis_options[:limits] = ((xmin - expand_x, xmax + expand_x), (ymin - expand_y, ymax + expand_y))
end
end

if length(axis_options) == 0
return Makie.SpecApi.GridLayout(
[facet_positions[name] => Makie.SpecApi.Axis(plots=plot_list_by_facet[name]) for name in facet_names]...,
facet_labels...,
facet_boxes...
)
else
return Makie.SpecApi.GridLayout(
[facet_positions[name] => Makie.SpecApi.Axis(plots=plot_list_by_facet[name]; axis_options...) for name in facet_names]...,
facet_labels...,
facet_boxes...
)
end
end

return Makie.SpecApi.GridLayout(
[k => Makie.SpecApi.Axis(plots=v; axis_options...) for
(k, v) in plot_list]...,
facet_labels...,
facet_boxes...
)
end


function draw_ggplot(plot::GGPlot)
axis = Makie.SpecApi.Axis(plot)
legend = build_legend(plot)
Expand Down
31 changes: 18 additions & 13 deletions src/facets.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
function facet_grid(args...; kwargs...)
if length(args) > 0
if length(args) > 0
throw("Use keyword args to specify rows and/or columns in facet_grid")
end

d = Dict(kwargs)

free_x = false
free_y = false

if haskey(d, :scales)
if d[:scales] == "free"
free_x = true
Expand All @@ -18,7 +18,7 @@ function facet_grid(args...; kwargs...)
free_y = true
end
end

return FacetOptions(
Symbol(get(d, :rows, nothing)),
Symbol(get(d, :cols, nothing)),
Expand All @@ -27,7 +27,7 @@ function facet_grid(args...; kwargs...)
get(d, :ncol, 3),
free_x,
free_y
)
)
end

function facet_wrap(args...; kwargs...)
Expand All @@ -43,7 +43,7 @@ function facet_wrap(args...; kwargs...)

free_x = false
free_y = false

if haskey(d, :scales)
if d[:scales] == "free"
free_x = true
Expand All @@ -63,19 +63,19 @@ function facet_wrap(args...; kwargs...)
get(d, :ncol, 3),
free_x,
free_y
)
)
end

function facet_wrap(plot::GGPlot, args...; kwargs...)
function facet_wrap(plot::GGPlot, args...; kwargs...)
return plot + facet_wrap(args...; kwargs...)
end

function facet_grid(plot::GGPlot, args...; kwargs...)
function facet_grid(plot::GGPlot, args...; kwargs...)
return plot + facet_grid(args...; kwargs...)
end

"""
Internal function. Given a list of names and (optionally) some constraints, return the relative position of the facets and their labels.
Internal function. Given a list of names and (optionally) some constraints, return the relative position of the facets and their labels.
"""
function position_facets(names, rows = nothing, cols = 3, labels = :all)
if (!isnothing(rows) && !isnothing(cols))
Expand All @@ -89,15 +89,17 @@ function position_facets(names, rows = nothing, cols = 3, labels = :all)
if isnothing(rows) && isnothing(cols)
throw("No constraints set for facet layout.")
elseif isnothing(rows) && !isnothing(cols)
rows = length(names) % cols == 0 ? length(names) ÷ cols : length(names) ÷ cols + 1
rows = length(names) % cols == 0 ?
length(names) ÷ cols : length(names) ÷ cols + 1
elseif !isnothing(rows) && isnothing(cols)
cols = length(names) % rows == 0 ? length(names) ÷ rows : length(names) ÷ rows + 1
cols = length(names) % rows == 0 ?
length(names) ÷ rows : length(names) ÷ rows + 1
end

# now we have a value for nrow and ncol

plot_positions = Dict{Any, Tuple}(name => (i, j) for (i, j, name) in zip(repeat(1:rows, inner = cols), repeat(1:cols, rows), names))

label_dict = Dict{Tuple, Any}()
box_dict = Dict{Tuple, Any}()

Expand All @@ -107,7 +109,10 @@ function position_facets(names, rows = nothing, cols = 3, labels = :all)
elseif labels == :top
label_dict = Dict{Tuple, Any}((i, j, Makie.Top()) => Makie.SpecApi.Label(text = name, padding = (8, 10, 8, 10)) for (i, j, name) in zip(repeat([1], cols), 1:cols, names))
box_dict = Dict{Tuple, Any}((i, j, Makie.Top()) => Makie.SpecApi.Box() for (i, j, name) in zip(repeat([1], cols), 1:cols, names))
elseif labels == :none
label_dict = Dict{Tuple, Any}()
box_dict = Dict{Tuple, Any}()
end

return (plot_positions, box_dict, label_dict)
end
end
4 changes: 2 additions & 2 deletions src/geoms/geom_boxplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ Compactly displays the distribution of continuous data.
ggplot(penguins, @aes(x=species, y=bill_length_mm)) +
geom_boxplot()

ggplot(penguins, @aes(x=species, y=bill_length_mm)) +
geom_boxplot(orientation=:horizontal)
ggplot(penguins, @aes(y=species, x=bill_length_mm)) +
geom_boxplot()

ggplot(penguins, @aes(x=species, y=bill_length_mm, dodge=sex, color=sex)) +
geom_boxplot()
Expand Down
Loading