diff --git a/Project.toml b/Project.toml index e8bf8e5..110e927 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TidierPlots" uuid = "337ecbd1-5042-4e2a-ae6f-ca776f97570a" authors = ["Randall Boyes <33524191+rdboyes@users.noreply.github.com> and contributors"] -version = "0.6.2" +version = "0.6.3" [deps] CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" diff --git a/src/TidierPlots.jl b/src/TidierPlots.jl index afd023b..fab30be 100644 --- a/src/TidierPlots.jl +++ b/src/TidierPlots.jl @@ -99,6 +99,11 @@ export scale_colour_continuous, scale_color_continuous export scale_colour_discrete, scale_color_discrete export scale_colour_manual, scale_color_manual +# transforms + +export cat_inseq, cat_inorder, number_on_axis, as_is, discard, verbatim, kernel_density_2d +export as_color + const plot_log = Ref{Bool}(true) const plot_show = Ref{Bool}(true) const plot_pluto = Ref{Bool}(true) diff --git a/src/aes.jl b/src/aes.jl index 2ea9202..509f501 100644 --- a/src/aes.jl +++ b/src/aes.jl @@ -1,7 +1,32 @@ function aes(args...; kwargs...) + col_transforms = Dict() + aes_args = Symbol[] + aes_kwargs = Dict{String, Symbol}() + + for arg in args + if arg isa Pair + push!(col_transforms, arg) + push!(aes_args, arg[1]) + else + push!(aes_args, Symbol(arg)) + end + end + d = Dict(kwargs) - return Aesthetics(Symbol.([args...]), - Dict([String(key) => Symbol(d[key]) for key in keys(d)])) + + for (k, v) in d + if v isa Pair + push!(col_transforms, v) + push!(aes_kwargs, String(k) => Symbol(v[1])) + else + push!(aes_kwargs, String(k) => Symbol(v)) + end + end + + return Aesthetics( + aes_args, + aes_kwargs, + col_transforms) end macro aes(exprs...) @@ -22,7 +47,7 @@ macro aes(exprs...) push!(positional, Symbol(aes_ex)) end end - return Aesthetics(positional, aes_dict) + return Aesthetics(positional, aes_dict, Dict()) end @eval const $(Symbol("@es")) = $(Symbol("@aes")) \ No newline at end of file diff --git a/src/extract_aes.jl b/src/extract_aes.jl index 1185da7..c217c23 100644 --- a/src/extract_aes.jl +++ b/src/extract_aes.jl @@ -2,6 +2,7 @@ function make_aes_extractor(required_aes) return function extract_aes(args, kwargs) aes_dict = Dict{String, Symbol}() args_dict = Dict{String, Any}() + transforms = nothing for arg in args if arg isa DataFrame @@ -21,13 +22,23 @@ function make_aes_extractor(required_aes) end end aes_dict = merge(aes_dict, arg.named) + transforms = arg.column_transformations end end + if !isnothing(transforms) + rev_aes_dict = Dict([v => k for (k, v) in aes_dict]) + transforms = Dict([Symbol(rev_aes_dict[k]) => [Symbol(rev_aes_dict[k])] => v for (k, v) in transforms]) + println(transforms) + else + println("TRANSFORMS EMPTY") + transforms = Dict{Symbol, Pair{Vector{Symbol}, AesTransform}}() + end + d = Dict(kwargs) args_dict = merge(args_dict, Dict([String(key) => d[key] for key in keys(d)])) - return (aes_dict, args_dict) + return (aes_dict, args_dict, transforms) end end diff --git a/src/geoms/geom_contour.jl b/src/geoms/geom_contour.jl index af9fc4d..f4e7521 100644 --- a/src/geoms/geom_contour.jl +++ b/src/geoms/geom_contour.jl @@ -11,7 +11,7 @@ end geom_tile = geom_template("geom_tile", ["x", "y", "z"], :Heatmap) geom_contour = geom_template("geom_contour", ["x", "y"], :Contour; aes_function = stat_density_2d, - column_transformations = Dict{Symbol, Pair{Vector{Symbol}, Function}}( + column_transformations = Dict{Symbol, Pair{Vector{Symbol}, AesTransform}}( :x => [:x]=>discard, :y => [:y]=>discard, :z => [:x, :y]=>kernel_density_2d)) diff --git a/src/geoms/geom_errorbar.jl b/src/geoms/geom_errorbar.jl index 3eed125..2d43983 100644 --- a/src/geoms/geom_errorbar.jl +++ b/src/geoms/geom_errorbar.jl @@ -1,5 +1,5 @@ function geom_errorbar(args...; kwargs...) - aes_dict, args_dict = extract_aes(args, kwargs) + aes_dict, args_dict, transforms = extract_aes(args, kwargs) args_dict["geom_name"] = "geom_errorbar" @@ -7,12 +7,12 @@ function geom_errorbar(args...; kwargs...) ["x", "ymin", "ymax"], # required aesthetics :Rangebars, # function for visual layer do_nothing, - Dict{Symbol, Pair{Vector{Symbol}, Function}}(); + transforms; special_aes = Dict("width" => "whiskerwidth")) end function geom_errorbarh(args...; kwargs...) - aes_dict, args_dict = extract_aes(args, kwargs) + aes_dict, args_dict, transforms = extract_aes(args, kwargs) args_dict["geom_name"] = "geom_errorbarh" args_dict["errorbar_direction"] = :x @@ -21,7 +21,7 @@ function geom_errorbarh(args...; kwargs...) ["y", "xmin", "xmax"], # required aesthetics :Rangebars, # function for visual layer do_nothing, - Dict{Symbol, Pair{Vector{Symbol}, Function}}(); + transforms; special_aes = Dict("width" => "whiskerwidth")) end diff --git a/src/geoms/geom_hvline.jl b/src/geoms/geom_hvline.jl index 739f084..ec680ea 100644 --- a/src/geoms/geom_hvline.jl +++ b/src/geoms/geom_hvline.jl @@ -1,5 +1,5 @@ function geom_hline(args...; kwargs...) - aes_dict, args_dict = extract_aes(args, kwargs) + aes_dict, args_dict, transforms = extract_aes(args, kwargs) args_dict["geom_name"] = "geom_hline" @@ -11,7 +11,8 @@ function geom_hline(args...; kwargs...) return build_geom(aes_dict, args_dict, ["yintercept"], # required aesthetics :HLines, - do_nothing, Dict{Symbol, Pair{Vector{Symbol}, Function}}()) # function for visual layer + do_nothing, + transforms) # function for visual layer end function geom_vline(args...; kwargs...) @@ -27,7 +28,8 @@ function geom_vline(args...; kwargs...) return build_geom(aes_dict, args_dict, ["xintercept"], # required aesthetics :VLines, - do_nothing, Dict{Symbol, Pair{Vector{Symbol}, Function}}()) # function for visual layer + do_nothing, + transforms) # function for visual layer end function geom_hline(plot::GGPlot, args...; kwargs...) diff --git a/src/geoms/geom_smooth.jl b/src/geoms/geom_smooth.jl index fb5ffe5..f83b40c 100644 --- a/src/geoms/geom_smooth.jl +++ b/src/geoms/geom_smooth.jl @@ -31,7 +31,7 @@ function geom_smooth(plot::GGPlot, args...; kwargs...) end function geom_smooth(args...; kwargs...) - aes_dict, args_dict = extract_aes(args, kwargs) + aes_dict, args_dict, transforms = extract_aes(args, kwargs) args_dict["geom_name"] = "geom_smooth" if haskey(args_dict, "method") @@ -41,13 +41,13 @@ function geom_smooth(args...; kwargs...) ["x", "y"], :Lines, stat_linear, - Dict{Symbol, Pair{Vector{Symbol}, Function}}()), + transforms), build_geom(aes_dict, args_dict, ["x", "lower", "upper"], :Band, stat_linear, - Dict{Symbol, Pair{Vector{Symbol}, Function}}())] + transforms)] end end @@ -56,7 +56,7 @@ function geom_smooth(args...; kwargs...) ["x", "y"], :Lines, stat_loess, - Dict{Symbol, Pair{Vector{Symbol}, Function}}()) + transforms) end function stat_loess(aes_dict::Dict{String, Symbol}, diff --git a/src/geoms/geom_template.jl b/src/geoms/geom_template.jl index a520b77..de339a1 100644 --- a/src/geoms/geom_template.jl +++ b/src/geoms/geom_template.jl @@ -2,13 +2,13 @@ function geom_template(name::AbstractString, required_aes::AbstractArray, spec_api_function::Symbol; aes_function::Function = do_nothing, - column_transformations::Dict{Symbol, Pair{Vector{Symbol}, Function}} = Dict{Symbol, Pair{Vector{Symbol}, Function}}(), + column_transformations::Dict{Symbol, Pair{Vector{Symbol}, AesTransform}} = Dict{Symbol, Pair{Vector{Symbol}, AesTransform}}(), extra_args::Dict = Dict()) extract_geom_aes = make_aes_extractor(required_aes) function geom_function(args...; kwargs...) - aes_dict, args_dict = extract_geom_aes(args, kwargs) + aes_dict, args_dict, transforms = extract_geom_aes(args, kwargs) args_dict["geom_name"] = name args_dict = merge(args_dict, extra_args) @@ -16,7 +16,7 @@ function geom_template(name::AbstractString, required_aes, spec_api_function, aes_function, - column_transformations) + merge(transforms, column_transformations)) end function geom_function(plot::GGPlot, args...; kwargs...) diff --git a/src/geoms/geom_text.jl b/src/geoms/geom_text.jl index a1be110..0b963a3 100644 --- a/src/geoms/geom_text.jl +++ b/src/geoms/geom_text.jl @@ -1,4 +1,4 @@ geom_text = geom_template("geom_text", ["x", "y"], :Text; - column_transformations = Dict{Symbol, Pair{Vector{Symbol}, Function}}(:text => [:text]=>verbatim)) + column_transformations = Dict{Symbol, Pair{Vector{Symbol}, AesTransform}}(:text => [:text]=>verbatim)) geom_label = geom_template("geom_label", ["x", "y"], :Text; - column_transformations = Dict{Symbol, Pair{Vector{Symbol}, Function}}(:text => [:text]=>verbatim)) \ No newline at end of file + column_transformations = Dict{Symbol, Pair{Vector{Symbol}, AesTransform}}(:text => [:text]=>verbatim)) \ No newline at end of file diff --git a/src/structs.jl b/src/structs.jl index 3823f71..d4c4bab 100644 --- a/src/structs.jl +++ b/src/structs.jl @@ -21,6 +21,7 @@ end struct Aesthetics positional::AbstractArray named::Dict + column_transformations::Dict end struct AxisOptions diff --git a/src/transforms.jl b/src/transforms.jl index b2cfc4e..3d171f3 100644 --- a/src/transforms.jl +++ b/src/transforms.jl @@ -14,13 +14,30 @@ struct PlottableData label_function::Any end +# AesTransform acts like a class of functions + +struct AesTransform + fn::Function +end + +# When called with the standard signature, an AesFunction object +# calls its internal function with those arguments + +(at::AesTransform)(target::Symbol, source::Vector{Symbol}, data::DataFrame) = at.fn(target, source, data) + +# When called with a string or symbol, as would happen in an aes() call +# returns a dict of the same type used in column_transformations + +(at::AesTransform)(sym::Symbol) = sym => at +(at::AesTransform)(str::String) = Symbol(str) => at + # simplest one is as_is, which just gets a column # exactly as it is in the DataFrame -function as_is(target::Symbol, source::Vector{Symbol}, data::DataFrame) +function as_is_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) return Dict{Symbol, PlottableData}( target => PlottableData( - data[!, source[1]], # get the column out of the dataframe + data[!, source[1]], # get the column out of the dataframe identity, # do nothing to it nothing, nothing @@ -28,6 +45,8 @@ function as_is(target::Symbol, source::Vector{Symbol}, data::DataFrame) ) end +as_is = AesTransform(as_is_fn) + # verbatim has a similar goal, but for String columns function convert_to(type::Type) @@ -43,9 +62,9 @@ function convert_to(type::Type) end end -verbatim = convert_to(String) +verbatim = AesTransform(convert_to(String)) -function number_on_axis(target::Symbol, source::Vector{Symbol}, data::DataFrame) +function number_on_axis_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) return Dict{Symbol, PlottableData}( target => PlottableData( data[!, source[1]], # get the column out of the dataframe @@ -56,11 +75,11 @@ function number_on_axis(target::Symbol, source::Vector{Symbol}, data::DataFrame) ) end - +number_on_axis = AesTransform(number_on_axis_fn) # categorical array handling options for String columns -function cat_inorder(target::Symbol, source::Vector{Symbol}, data::DataFrame) +function cat_inorder_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) cat_column = data[!, source[1]] cat_array = CategoricalArray(cat_column, levels = unique(cat_column), @@ -80,7 +99,9 @@ function cat_inorder(target::Symbol, source::Vector{Symbol}, data::DataFrame) ) end -function cat_inseq(target::Symbol, source::Vector{Symbol}, data::DataFrame) +cat_inorder = AesTransform(cat_inorder_fn) + +function cat_inseq_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) cat_array = CategoricalArray(data[!, source[1]]) label_target = target == :x ? :xticks : @@ -97,9 +118,11 @@ function cat_inseq(target::Symbol, source::Vector{Symbol}, data::DataFrame) ) end +cat_inseq = AesTransform(cat_inseq_fn) + # kernel density estimation for geom_contour -function kernel_density_2d(target::Symbol, source::Vector{Symbol}, data::DataFrame) +function kernel_density_2d_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) k = kde((data[!, source[1]], data[!, source[2]])) @@ -115,12 +138,16 @@ function kernel_density_2d(target::Symbol, source::Vector{Symbol}, data::DataFra return return_dict end +kernel_density_2d = AesTransform(kernel_density_2d_fn) + # returns nothing, removing aes from graph -function discard(target::Symbol, source::Vector{Symbol}, data::DataFrame) +function discard_fn(target::Symbol, source::Vector{Symbol}, data::DataFrame) return Dict{Symbol, PlottableData}() end +discard = AesTransform(discard_fn) + # tweaks # takes an existing PlottableData object and modifies the makie_function