From 5085ba151812d3eb1839f6c23fd4707799cae784 Mon Sep 17 00:00:00 2001 From: Randy Boyes Date: Wed, 18 Sep 2024 11:18:20 -0400 Subject: [PATCH 1/2] fixes facets and grouping --- Project.toml | 1 + src/draw.jl | 165 ++++++++++++++++++++++---------------- src/facets.jl | 31 ++++--- src/geoms/geom_boxplot.jl | 4 +- 4 files changed, 117 insertions(+), 84 deletions(-) diff --git a/Project.toml b/Project.toml index 203e7ac..bef8414 100644 --- a/Project.toml +++ b/Project.toml @@ -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] diff --git a/src/draw.jl b/src/draw.jl index 17f9c08..5ab5861 100644 --- a/src/draw.jl +++ b/src/draw.jl @@ -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}() @@ -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, @@ -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 @@ -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) diff --git a/src/facets.jl b/src/facets.jl index 2289dc8..218fc97 100644 --- a/src/facets.jl +++ b/src/facets.jl @@ -1,5 +1,5 @@ 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 @@ -7,7 +7,7 @@ function facet_grid(args...; kwargs...) free_x = false free_y = false - + if haskey(d, :scales) if d[:scales] == "free" free_x = true @@ -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)), @@ -27,7 +27,7 @@ function facet_grid(args...; kwargs...) get(d, :ncol, 3), free_x, free_y - ) + ) end function facet_wrap(args...; kwargs...) @@ -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 @@ -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)) @@ -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}() @@ -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 \ No newline at end of file +end diff --git a/src/geoms/geom_boxplot.jl b/src/geoms/geom_boxplot.jl index e2171b1..b1f3121 100644 --- a/src/geoms/geom_boxplot.jl +++ b/src/geoms/geom_boxplot.jl @@ -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() From bf37ff57d9c1f97441f33b9ac392eed5434db90d Mon Sep 17 00:00:00 2001 From: Randy Boyes Date: Wed, 18 Sep 2024 11:26:10 -0400 Subject: [PATCH 2/2] readme updates --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bee4a30..99ad5d8 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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` @@ -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. @@ -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) @@ -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()