diff --git a/Project.toml b/Project.toml index 1f2a04f..4bf2916 100644 --- a/Project.toml +++ b/Project.toml @@ -17,7 +17,6 @@ RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" @@ -35,7 +34,6 @@ RecipesBase = "0.6, 0.7, 0.8, 1" RecipesPipeline = "0.1.6, 0.2, 0.3" Reexport = "0.2, 1.0" StatsBase = "0.32, 0.33" -TableOperations = "0.2, 0.3, 1.0" Tables = "1" Widgets = "0.5, 0.6" julia = "1" diff --git a/src/StatsPlots.jl b/src/StatsPlots.jl index 4c17743..5ba6f4a 100644 --- a/src/StatsPlots.jl +++ b/src/StatsPlots.jl @@ -3,7 +3,6 @@ module StatsPlots using Reexport import RecipesBase: recipetype import Tables -import TableOperations using RecipesPipeline @reexport using Plots import Plots: _cycle @@ -27,9 +26,8 @@ import KernelDensity @shorthands cdensity -export @df, dataviewer +export dataviewer -include("df.jl") include("interact.jl") include("corrplot.jl") include("cornerplot.jl") diff --git a/src/df.jl b/src/df.jl deleted file mode 100644 index c87bf54..0000000 --- a/src/df.jl +++ /dev/null @@ -1,208 +0,0 @@ -""" - `@df d x` - -Convert every symbol in the expression `x` with the respective column in `d` if it exists. - -If you want to avoid replacing the symbol, escape it with `^`. - -`NA` values are replaced with `NaN` for columns of `Float64` and `""` or `Symbol()` -for strings and symbols respectively. - -`x` can be either a plot command or a block of plot commands. -""" -macro df(d, x) - esc(Expr(:call, df_helper(x), d)) -end - -""" - `@df x` - -Curried version of `@df d x`. Outputs an anonymous function `d -> @df d x`. -""" -macro df(x) - esc(df_helper(x)) -end - -function df_helper(x) - i = gensym() - Expr(:(->), i, df_helper(i, x)) -end - -function df_helper(d, x) - if isa(x, Expr) && x.head == :block # meaning that there were multiple plot commands - commands = [df_helper(d, xx) for xx in x.args if !(isa(xx, Expr) && xx.head == :line || isa(xx, LineNumberNode))] # apply the helper recursively - return Expr(:block, commands...) - - elseif isa(x, Expr) && x.head == :call # each function call is operated on alone - syms = Any[] - vars = Symbol[] - plot_call = parse_table_call!(d, x, syms, vars) - names = gensym() - compute_vars = Expr(:(=), Expr(:tuple, Expr(:tuple, vars...), names), - Expr(:call, :($(@__MODULE__).extract_columns_and_names), d, syms...)) - argnames = _argnames(names, x) - if (length(plot_call.args) >= 2) && isa(plot_call.args[2], Expr) && (plot_call.args[2].head == :parameters) - label_plot_call = Expr(:call, :($(@__MODULE__).add_label), plot_call.args[2], argnames, - plot_call.args[1], plot_call.args[3:end]...) - else - label_plot_call = Expr(:call, :($(@__MODULE__).add_label), argnames, plot_call.args...) - end - return Expr(:block, compute_vars, label_plot_call) - - else - error("Second argument ($x) can only be a block or function call") - end -end - -parse_table_call!(d, x, syms, vars) = x - -function parse_table_call!(d, x::QuoteNode, syms, vars) - new_var = gensym(x.value) - push!(syms, x) - push!(vars, new_var) - return new_var -end - - -function parse_table_call!(d, x::Expr, syms, vars) - if x.head == :. && length(x.args) == 2 - isa(x.args[2], QuoteNode) && return x - elseif x.head == :call - x.args[1] == :^ && length(x.args) == 2 && return x.args[2] - if x.args[1] == :cols - if length(x.args) == 1 - push!(x.args, :($(@__MODULE__).column_names($d))) - return parse_table_call!(d, x, syms, vars) - end - range = x.args[2] - new_vars = gensym("range") - push!(syms, range) - push!(vars, new_vars) - return new_vars - end - elseif x.head==:braces # From Query: use curly brackets to simplify writing named tuples - new_ex = Expr(:tuple, x.args...) - - for (j,field_in_NT) in enumerate(new_ex.args) - if isa(field_in_NT, Expr) && field_in_NT.head==:(=) - new_ex.args[j] = Expr(:(=), field_in_NT.args...) - elseif field_in_NT isa QuoteNode - new_ex.args[j] = Expr(:(=), field_in_NT.value, field_in_NT) - elseif isa(field_in_NT, Expr) - new_ex.args[j] = Expr(:(=), Symbol(filter(t -> t != ':', string(field_in_NT))), field_in_NT) - elseif isa(field_in_NT, Symbol) - new_ex.args[j] = Expr(:(=), field_in_NT, field_in_NT) - end - end - return parse_table_call!(d, new_ex, syms, vars) - end - return Expr(x.head, (parse_table_call!(d, arg, syms, vars) for arg in x.args)...) -end - -function column_names(t) - s = Tables.schema(t) - s === nothing ? propertynames(first(Tables.rows(t))) : s.names -end - -not_kw(x) = true -not_kw(x::Expr) = !(x.head in [:kw, :parameters]) - -function insert_kw!(x::Expr, s::Symbol, v) - index = isa(x.args[2], Expr) && x.args[2].head == :parameters ? 3 : 2 - x.args = vcat(x.args[1:index-1], Expr(:kw, s, v), x.args[index:end]) -end - -function _argnames(names, x::Expr) - Expr(:vect, [_arg2string(names, s) for s in x.args[2:end] if not_kw(s)]...) -end - -_arg2string(names, x) = stringify(x) -function _arg2string(names, x::Expr) - if x.head == :call && x.args[1] == :cols - return :($(@__MODULE__).compute_name($names, $(x.args[2]))) - elseif x.head == :call && x.args[1] == :hcat - return hcat(stringify.(x.args[2:end])...) - elseif x.head == :hcat - return hcat(stringify.(x.args)...) - else - return stringify(x) - end -end - -stringify(x) = filter(t -> t != ':', string(x)) - -compute_name(names, i::Int) = names[i] -compute_name(names, i::Symbol) = i -compute_name(names, i) = reshape([compute_name(names, ii) for ii in i], 1, :) - -""" - add_label(argnames, f, args...; kwargs...) - -This function ensures that labels are passed to the plotting command, if it accepts them. - -If `f` does not accept keyword arguments, and `kwargs` is empty, it will only -forward `args...`. - -If the user has provided keyword arguments, but `f` does not accept them, -then it will error. -""" -function add_label(argnames, f, args...; kwargs...) - i = findlast(t -> isa(t, Expr) || isa(t, AbstractArray), argnames) - try - if (i === nothing) - return f(args...; kwargs...) - else - return f(label = stringify.(argnames[i]), args...; kwargs...) - end - catch e - if e isa MethodError || - (e isa ErrorException && occursin("does not accept keyword arguments", e.msg)) - # check if the user has supplied kwargs, then we need to rethrow the error - isempty(kwargs) || rethrow(e) - # transmit only args to `f` - return f(args...) - else - rethrow(e) - end - end -end - -get_col(s::Int, col_nt, names) = col_nt[names[s]] -get_col(s::Symbol, col_nt, names) = get(col_nt, s, s) -get_col(syms, col_nt, names) = hcat((get_col(s, col_nt, names) for s in syms)...) - -# get the appropriate name when passed an Integer -add_sym!(cols, i::Integer, names) = push!(cols, names[i]) -# check for errors in Symbols -add_sym!(cols, s::Symbol, names) = s in names ? push!(cols, s) : cols -# recursively extract column names -function add_sym!(cols, s, names) - for si in s - add_sym!(cols, si, names) - end - cols -end - -""" - extract_columns_and_names(df, syms...) - -Extracts columns and their names (if the column number is an integer) -into a slightly complex `Tuple`. - -The structure goes as `((columndata...), names)`. This is unpacked by the [`@df`](@ref) macro into `gensym`'ed variables, which are passed to the plotting function. - -!!! note - If you want to extend the [`@df`](@ref) macro - to work with your custom type, this is the - function you should overload! -""" -function extract_columns_and_names(df, syms...) - Tables.istable(df) || error("Only tables are supported") - names = column_names(df) - - # extract selected column names - selected_cols = add_sym!(Symbol[], syms, names) - - cols = Tables.columntable(TableOperations.select(df, unique(selected_cols)...)) - return Tuple(get_col(s, cols, names) for s in syms), names -end