Skip to content

Commit

Permalink
add highlight transformation
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumbiegel committed Sep 5, 2024
1 parent 4834dea commit 8f07178
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 3 deletions.
3 changes: 2 additions & 1 deletion src/AlgebraOfGraphics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export hideinnerdecorations!, deleteemptyaxes!
export Layer, Layers, ProcessedLayer, ProcessedLayers
export Entry, AxisEntries
export renamer, sorter, nonnumeric, verbatim, presorted
export density, histogram, linear, smooth, expectation, frequency, contours, filled_contours
export density, histogram, linear, smooth, expectation, frequency, contours, filled_contours, highlight
export visual, data, geodata, dims, mapping
export datetimeticks
export draw, draw!
Expand Down Expand Up @@ -69,6 +69,7 @@ include("transformations/frequency.jl")
include("transformations/expectation.jl")
include("transformations/contours.jl")
include("transformations/filled_contours.jl")
include("transformations/highlight.jl")
include("guides/guides.jl")
include("guides/legend.jl")
include("guides/colorbar.jl")
Expand Down
18 changes: 17 additions & 1 deletion src/algebra/layer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ end

unnest(vs::AbstractArray, indices) = map(k -> [el[k] for el in vs], indices)

unnest_arrays(vs) = unnest(vs, keys(first(vs)))
unnest_arrays(vs) = isempty(vs) ? [[]] : unnest(vs, keys(first(vs)))
function unnest_dictionaries(vs)
isempty(vs) && return Dictionary()
return Dictionary(Dict((k => [el[k] for el in vs] for k in collect(keys(first(vs))))))
end
slice(v, c) = map(el -> getnewindex(el, c), v)
Expand All @@ -192,6 +193,21 @@ function Base.map(f, processedlayer::ProcessedLayer)
return ProcessedLayer(processedlayer; positional, named)
end

function filtermap(f, processedlayer::ProcessedLayer)
axs = shape(processedlayer)
outputs = map(CartesianIndices(axs)) do c
return f(slice(processedlayer.positional, c), slice(processedlayer.named, c))
end
to_remove = outputs .== nothing
deleteat!(outputs, to_remove)
primary = map(processedlayer.primary) do value
value[.!to_remove]
end

positional, named = unnest_arrays(map(first, outputs)), unnest_dictionaries(map(last, outputs))
return ProcessedLayer(processedlayer; positional, named, primary)
end

## Get scales from a `ProcessedLayer`

function uniquevalues(v::AbstractArray)
Expand Down
5 changes: 4 additions & 1 deletion src/guides/legend.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ end

categorical_scales_mergeable(c1, c2) = false # there can be continuous scales in the mix, like markersize

is_empty_categorical_scale(s::CategoricalScale) = isempty(datavalues(s))
is_empty_categorical_scale(s::ContinuousScale) = false

function compute_legend(grid::Matrix{AxisEntries}; order::Union{Nothing,AbstractVector})
# gather valid named scales
scales_categorical = legendable_scales(Val(:categorical), first(grid).categoricalscales)
Expand All @@ -100,7 +103,7 @@ function compute_legend(grid::Matrix{AxisEntries}; order::Union{Nothing,Abstract
scales = Iterators.flatten((pairs(scales_categorical), pairs(scales_continuous)))

# if no legendable scale is present, return nothing
isempty(scales) && return nothing
isempty(scales) || all(x -> all(is_empty_categorical_scale, last(x)), scales) && return nothing

scales_by_symbol = Dictionary{Symbol,ScaleWithMeta}()

Expand Down
51 changes: 51 additions & 0 deletions src/transformations/highlight.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
struct Highlight{F<:Function}
target::Vector{Union{Int,Symbol}}
predicate::F
repeat_facets::Bool
end

function Highlight(
target::Vector,
predicate;
repeat_facets = false
)
return Highlight(
convert(Vector{Union{Int,Symbol}}, target),
predicate,
repeat_facets
)
end

function (highlight::Highlight)(p::ProcessedLayer)
primary = AlgebraOfGraphics.dictionary([(key == :color ? :group : key, value) for (key, value) in pairs(p.primary)
# should the data be repeated across all facets or not? maybe needs to be an option
if !(highlight.repeat_facets && key in (:layout, :row, :col))
])

grayed_out = ProcessedLayer(p; primary, attributes = merge(p.attributes, AlgebraOfGraphics.dictionary([:color => :gray80])))

function apply_predicate(target::Vector, predicate::Function, positional, named, scalar_primaries)
b = predicate([resolve_target(t, positional, named, scalar_primaries) for t in target]...)
b isa Bool || error("Highlighting predicate returned non-boolean value $b")
return b
end

resolve_target(target::Int, positional, named, scalar_primaries) = positional[target]
resolve_target(target::Symbol, positional, named, scalar_primaries) = haskey(scalar_primaries, target) ? scalar_primaries[target] : named[target]


i::Int = 0
colored = AlgebraOfGraphics.filtermap(p) do positional, named
i += 1
scalar_primaries = map(val -> val[i], p.primary)
if apply_predicate(highlight.target, highlight.predicate, positional, named, scalar_primaries)
return positional, named
else
return nothing
end
end

return ProcessedLayers([grayed_out, colored])
end

highlight(pair::Pair; kwargs...) = AlgebraOfGraphics.transformation(Highlight(vcat(pair[1]), pair[2]; kwargs...))

0 comments on commit 8f07178

Please sign in to comment.