diff --git a/Project.toml b/Project.toml index 0a9b901..65df7a5 100644 --- a/Project.toml +++ b/Project.toml @@ -28,3 +28,15 @@ Distributions = "0.25" FixedPointNumbers = "0.8" Unitful = "1.19" julia = "1.10" + +[extras] +CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" +ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[targets] +test = ["Test", "CategoricalArrays", "ColorSchemes", "Colors", "Distributions", "FixedPointNumbers", "Unitful"] diff --git a/README.md b/README.md index decc909..aee7b1b 100644 --- a/README.md +++ b/README.md @@ -3,55 +3,25 @@ [![Build Status](https://github.com/JuliaGraphics/Colorfy.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaGraphics/Colorfy.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Coverage](https://codecov.io/gh/JuliaGraphics/Colorfy.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaGraphics/Colorfy.jl) -Colorfy.jl is a package for mapping Julia objects into colors defined by [Colors.jl](https://github.com/JuliaGraphics/Colors.jl). +Colorfy.jl is a package for mapping Julia objects into colors +defined by [Colors.jl](https://github.com/JuliaGraphics/Colors.jl). ## Usage -The use of this package is centralized in the `Colorfier` struct. -This type stores the necessary info to convert a list of values into a list of colors. -To extract the mapped colors from the colorfier, use the `Colorfy.colors` function: - -```julia -julia> values = rand(5) -5-element Vector{Float64}: - 0.6725922579880301 - 0.48419175677473947 - 0.555196651363384 - 0.9391048282597594 - 0.06139286380440967 - -julia> colorfier = Colorfier(values); - -julia> Colorfy.colors(colorfier) -5-element Array{RGBA{Float64},1} with eltype ColorTypes.RGBA{Float64}: - RGBA{Float64}(0.25686927730139364,0.744083158641258,0.4461059964847054,1.0) - RGBA{Float64}(0.13396134240010413,0.5479273380822499,0.5536215374839386,1.0) - RGBA{Float64}(0.12033938235581829,0.6238620558326493,0.534269659829833,1.0) - RGBA{Float64}(0.993248,0.906157,0.143936,1.0) - RGBA{Float64}(0.267004,0.004874,0.329415,1.0) - -julia> colorfier = Colorfier(values, alphas=0.5); - -julia> Colorfy.colors(colorfier) -5-element Array{RGBA{Float64},1} with eltype ColorTypes.RGBA{Float64}: - RGBA{Float64}(0.25686927730139364,0.744083158641258,0.4461059964847054,0.5) - RGBA{Float64}(0.13396134240010413,0.5479273380822499,0.5536215374839386,0.5) - RGBA{Float64}(0.12033938235581829,0.6238620558326493,0.534269659829833,0.5) - RGBA{Float64}(0.993248,0.906157,0.143936,0.5) - RGBA{Float64}(0.267004,0.004874,0.329415,0.5) -``` - -For convenience, the `colorfy` function is defined. This function -creates a `Colorfier` instance and calls the `Colorfy.colors` function: +The `colorfy` function takes a vector of values with options +(e.g., `alpha`, `colorscheme`) and converts them into valid colors: ```julia julia> values = [:red, :green, :blue]; -julia> colorfy(values, alphas=[0.5, 0.6, 0.7]) +julia> colorfy(values, alpha=[0.5, 0.6, 0.7]) 3-element Array{RGBA{N0f8},1} with eltype ColorTypes.RGBA{FixedPointNumbers.N0f8}: RGBA{N0f8}(1.0,0.0,0.0,0.5) RGBA{N0f8}(0.0,0.502,0.0,0.6) RGBA{N0f8}(0.0,0.0,1.0,0.7) ``` -See the `Colorfier` docstring for a description of all options. +Please check the `colorfy` docstring for more details. + +Developers can register colorful representations for their +types by implementing methods for the `Colorfy.repr` function. diff --git a/ext/ColorfyCategoricalArraysExt.jl b/ext/ColorfyCategoricalArraysExt.jl index 72a9df6..f1bd9d6 100644 --- a/ext/ColorfyCategoricalArraysExt.jl +++ b/ext/ColorfyCategoricalArraysExt.jl @@ -4,17 +4,15 @@ module ColorfyCategoricalArraysExt -using Colorfy -using Colorfy: Values -using ColorSchemes: colorschemes -using CategoricalArrays: CategoricalValue, levels, levelcode +using CategoricalArrays: CategoricalValue +using CategoricalArrays: levels, levelcode -function Colorfy.getcolors(colorfier::Colorfier{<:Values{CategoricalValue}}) - values = Colorfy.values(colorfier) - colorscheme = Colorfy.colorscheme(colorfier) +import Colorfy + +function Colorfy.repr(values::AbstractVector{<:CategoricalValue}, colorscheme, colorrange) nlevels = length(levels(values)) - categcolors = colorscheme[range(0, nlevels > 1 ? 1 : 0, length=nlevels)] - categcolors[levelcode.(values)] + lcolors = colorscheme[range(0, nlevels > 1 ? 1 : 0, length=nlevels)] + lcolors[levelcode.(values)] end end diff --git a/ext/ColorfyDistributionsExt.jl b/ext/ColorfyDistributionsExt.jl index 596a20a..c53fd00 100644 --- a/ext/ColorfyDistributionsExt.jl +++ b/ext/ColorfyDistributionsExt.jl @@ -4,24 +4,30 @@ module ColorfyDistributionsExt -using Colorfy -using Colorfy: Values -using Distributions: Distribution, location, scale - -function Colorfy.getcolors(colorfier::Colorfier{<:Values{Distribution}}) - values = location.(Colorfy.values(colorfier)) - dcolorfier = Colorfy.update(colorfier; values) - Colorfy.getcolors(dcolorfier) -end +using Distributions: Distribution +using Distributions: location, scale +using Colors: coloralpha + +import Colorfy + +function Colorfy.repr(values::AbstractVector{<:Distribution}, colorscheme, colorrange) + # extract location and scale parameters + μs = location.(values) + σs = scale.(values) -function Colorfy.defaultalphas(values::Values{Distribution}) - s = scale.(values) - a, b = extrema(s) - if a == b - fill(1, length(values)) + # compute alphas based on scale parameters + a, b = extrema(σs) + αs = if a == b + fill(1.0, length(μs)) else - @. 1 - (s - a) / (b - a) + @. 1.0 - (σs - a) / (b - a) end + + # get colors for location parameters + cs = Colorfy.repr(μs, colorscheme, colorrange) + + # apply alphas to colors + coloralpha.(cs, αs) end end diff --git a/ext/ColorfyUnitfulExt.jl b/ext/ColorfyUnitfulExt.jl index 34639c3..10e9f4c 100644 --- a/ext/ColorfyUnitfulExt.jl +++ b/ext/ColorfyUnitfulExt.jl @@ -4,15 +4,13 @@ module ColorfyUnitfulExt -using Colorfy -using Colorfy: Values -using Unitful: Quantity, ustrip +using Unitful: Quantity +using Unitful: ustrip -function Colorfy.getcolors(colorfier::Colorfier{<:Values{Quantity}}) - values = ustrip.(Colorfy.values(colorfier)) - ucolorfier = Colorfy.update(colorfier; values) - Colorfy.getcolors(ucolorfier) -end +import Colorfy + +Colorfy.repr(values::AbstractVector{<:Quantity}, colorscheme, colorrange) = + Colorfy.repr(ustrip.(values), colorscheme, colorrange) Colorfy.ascolorrange(colorrange::NTuple{2,Quantity}) = Colorfy.ascolorrange(ustrip.(colorrange)) diff --git a/src/Colorfy.jl b/src/Colorfy.jl index b4bd44c..9bb46ee 100644 --- a/src/Colorfy.jl +++ b/src/Colorfy.jl @@ -9,148 +9,58 @@ using ColorSchemes using FixedPointNumbers using Dates -export Colorfier, colorfy - -# type alias to reduce typing -const Values{T} = AbstractVector{<:T} +export colorfy """ - Colorfier(values; [alphas, colorscheme, colorrange]) + colorfy(values; alpha=1.0, colorscheme=:viridis, colorrange=:extrema) -Maps each value in `values` to a color. Colors can be obtained using the [`Colorfy.colors`](@ref) function. +Convert `values` to Colors.jl colors based on given options. ## Options -* `alphas` - Scalar or a vector of color alphas (default to `Colorfy.defaultalphas(values)`); -* `colorscheme` - Color scheme specification (default to `Colorfy.defaultcolorscheme(values)`); -* `colorrange` - Tuple with minimum and maximum color values or a symbol that can be passed - to the `rangescale` argument of the `ColorSchemes.get` function (default to `Colorfy.defaultcolorrange(values)`); -""" -struct Colorfier{V,A,S,R} - values::V - alphas::A - colorscheme::S - colorrange::R -end - -function Colorfier(values; alphas=nothing, colorscheme=nothing, colorrange=nothing) - values′ = asvalues(values) - alphas′ = isnothing(alphas) ? defaultalphas(values′) : alphas - colorscheme′ = isnothing(colorscheme) ? defaultcolorscheme(values′) : colorscheme - colorrange′ = isnothing(colorrange) ? defaultcolorrange(values′) : colorrange - Colorfier(values′, asalphas(alphas′, values′), ascolorscheme(colorscheme′), ascolorrange(colorrange′)) -end - -""" - colorfy(values; kwargs...) - -Shortcut to `Colorfy.colors(Colorfier(values; kwargs...))` for convenience. - -See also [`Colorfier`](@ref), [`Colorfy.colors`](@ref). -""" -colorfy(values; kwargs...) = colors(Colorfier(values; kwargs...)) - -""" - Colorfy.update(colorfier; [values, alphas, colorscheme, colorrange]) - -Constructs a new colorfier with `colorfier` fields and updated fields passed as keyword arguments. -""" -update( - colorfier::Colorfier; - values=values(colorfier), - alphas=alphas(colorfier), - colorscheme=colorscheme(colorfier), - colorrange=colorrange(colorfier) -) = Colorfier(values; alphas, colorscheme, colorrange) - -# -------- -# GETTERS -# -------- - -""" - Colorfy.values(colorfier) - -Values of the `colorfier`. +* `alpha` - Scalar or vector of transparency values between 0.0 and 1.0 +* `colorscheme` - Color scheme from ColorSchemes.jl (e.g., "viridis", ["black", "white"]) +* `colorrange` - Minimum and maximum values or a symbol (see `ColorSchemes.get`) """ -values(colorfier::Colorfier) = colorfier.values +function colorfy(values; alpha=1.0, colorscheme=:viridis, colorrange=:extrema) + # handle input arguments + vs, αs, s, r = handleargs(values, alpha, colorscheme, colorrange) -""" - Colorfy.alphas(colorfier) - -Color alphas of the `colorfier`. -""" -alphas(colorfier::Colorfier) = colorfier.alphas - -""" - Colorfy.colorscheme(colorfier) - -Color scheme of the `colorfier`. -""" -colorscheme(colorfier::Colorfier) = colorfier.colorscheme - -""" - Colorfy.colorrange(colorfier) - -Color range of the `colorfier`. -""" -colorrange(colorfier::Colorfier) = colorfier.colorrange - -# ---- -# API -# ---- - -""" - Colorfy.defaultalphas(values) - -Default color alphas for `values`. -""" -function defaultalphas(values::Values) - minds = findall(ismissing, values) - vinds = setdiff(1:length(values), minds) - - if isempty(minds) - fill(1, length(values)) - else - valphas = defaultalphas(nonmissingvec(values[vinds])) - malpha = zero(eltype(valphas)) - genvec(vinds, valphas, minds, malpha, length(values)) + # find invalid and valid indices + iinds = findall(isinvalid, vs) + vinds = setdiff(1:length(vs), iinds) + + # if all values are invalid, return transparent colors + isempty(vinds) && return fill(colorant"transparent", length(values)) + + # construct colors for valid values + rcolors = repr(nonmissingvec(vs[vinds]), s, r) + ralphas = map(Colors.alpha, rcolors) + vcolors = coloralpha.(rcolors, αs[vinds] .* ralphas) + + # construct colors for all values + if isempty(iinds) # all values are valid, return colors directly + vcolors + else # set "transparent" color for invalid values + genvec(vinds, vcolors, iinds, colorant"transparent", length(values)) end end -defaultalphas(values::Values{Colorant}) = alpha.(values) - -""" - Colorfy.defaultcolorscheme(values) - -Default color scheme for `values`. -""" -defaultcolorscheme(_) = colorschemes[:viridis] - -""" - Colorfy.defaultcolorrange(values) - -Default color range for `values`. -""" -defaultcolorrange(_) = :extrema - -""" - Colorfy.asvalues(values) +function handleargs(values, alphas, colorscheme, colorrange) + vs = asvalues(values) + αs = asalphas(alphas, vs) + s = ascolorscheme(colorscheme) + r = ascolorrange(colorrange) + vs, αs, s, r +end -Valid color values for a given `values`. -""" asvalues(values) = values -asvalues(values::Values{Colorant}) = values -asvalues(values::Values{Colorant{Q0f7}}) = fixcolors(values) -asvalues(values::Values{Colorant{Q0f15}}) = fixcolors(values) -asvalues(values::Values{Colorant{Q0f31}}) = fixcolors(values) -asvalues(values::Values{Colorant{Q0f63}}) = fixcolors(values) - -""" - Colorfy.asalphas(alphas, values) +asvalues(values::AbstractVector{<:Colorant{Q0f7}}) = fixcolors(values) +asvalues(values::AbstractVector{<:Colorant{Q0f15}}) = fixcolors(values) +asvalues(values::AbstractVector{<:Colorant{Q0f31}}) = fixcolors(values) +asvalues(values::AbstractVector{<:Colorant{Q0f63}}) = fixcolors(values) -Valid color alphas for a given `alphas` and `values`. -""" -asalphas(alpha, values) = fill(alpha, length(values)) +asalphas(alpha::Number, values) = fill(alpha, length(values)) function asalphas(alphas::AbstractVector, values) if length(alphas) ≠ length(values) throw(ArgumentError("the number of alphas must be equal to the number of values")) @@ -158,94 +68,55 @@ function asalphas(alphas::AbstractVector, values) alphas end -""" - Colorfy.ascolorscheme(colorscheme) - -Valid `ColorScheme` object for a given `colorscheme` specification. -""" ascolorscheme(colorscheme::Symbol) = colorschemes[colorscheme] ascolorscheme(colorscheme::AbstractString) = ascolorscheme(Symbol(colorscheme)) ascolorscheme(colorscheme::AbstractVector) = ColorScheme([parse(Colorant, color) for color in colorscheme]) ascolorscheme(colorscheme::ColorScheme) = colorscheme -""" - Colorfy.ascolorrange(colorrange) - -Valid color range, accepted by the `ColorSchemes.get` function, for a given `colorrange`. -""" ascolorrange(colorrange::Symbol) = colorrange ascolorrange(colorrange::NTuple{2,T}) where {T<:Real} = colorrange ascolorrange(colorrange::NTuple{2,Real}) = promote(colorrange...) -""" - Colorfy.colors(colorfier) - -Colors mapped from the `colorfier` . -""" -function colors(colorfier::Colorfier) - # find invalid and valid indices - isinvalid(v) = ismissing(v) || (v isa Number && !isfinite(v)) - vals = values(colorfier) - iinds = findall(isinvalid, vals) - vinds = setdiff(1:length(vals), iinds) - - if isempty(iinds) - # required to handle Vector{Union{Missing,T}} without missing values - vvals = nonmissingvec(vals) - vcolorfier = update(colorfier, values=vvals) - coloralpha.(getcolors(vcolorfier), alphas(vcolorfier)) - else - # get valid colors and set "transparent" for invalid values - vvals = nonmissingvec(vals[vinds]) - valphas = alphas(colorfier)[vinds] - vcolorfier = update(colorfier, values=vvals, alphas=valphas) - vcolors = colors(vcolorfier) - icolor = colorant"transparent" - genvec(vinds, vcolors, iinds, icolor, length(vals)) - end -end +# ---------------- +# IMPLEMENTATIONS +# ---------------- """ - Colorfy.getcolors(colorfier, values) + repr(values::AbstractVector{T}, colorscheme, colorrange) -Function intended for developers that returns the mapped colors from the `colorfier` without the alphas. -Alphas are applied in the `Colorfy.colors` function. +Colorful representation of `values` of type `T` based on `colorscheme` and `colorrange`. """ -function getcolors(colorfier::Colorfier) +function repr(values::AbstractVector{T}, colorscheme, colorrange) where {T} throw(ArgumentError(""" - Values of type `$(eltype(colorfier.values))` are not supported. - Please make sure your vector of colors has a concrete type. - """)) + values of type `$T` do not have a colorful representation. + + Please make sure your vector has a concrete element type + and that a `Colorfy.repr` method exists for it. + """)) end -getcolors(colorfier::Colorfier{<:Values{Number}}) = - get(colorscheme(colorfier), values(colorfier), colorrange(colorfier)) +repr(values::AbstractVector{<:Colorant}, colorscheme, colorrange) = values -getcolors(colorfier::Colorfier{<:Values{AbstractString}}) = parse.(Ref(Colorant), values(colorfier)) +repr(values::AbstractVector{<:Number}, colorscheme, colorrange) = get(colorscheme, values, colorrange) -getcolors(colorfier::Colorfier{<:Values{Symbol}}) = parse.(Ref(Colorant), string.(values(colorfier))) +repr(values::AbstractVector{<:Symbol}, colorscheme, colorrange) = repr(string.(values), colorscheme, colorrange) -getcolors(colorfier::Colorfier{<:Values{Colorant}}) = values(colorfier) +repr(values::AbstractVector{<:AbstractString}, colorscheme, colorrange) = parse.(Ref(Colorant), values) -function getcolors(colorfier::Colorfier{<:Values{DateTime}}) - dvalues = datetime2unix.(values(colorfier)) - dcolorfier = update(colorfier, values=dvalues) - getcolors(dcolorfier) -end +repr(values::AbstractVector{<:Date}, colorscheme, colorrange) = repr(DateTime.(values), colorscheme, colorrange) -function getcolors(colorfier::Colorfier{<:Values{Date}}) - dvalues = map(d -> datetime2unix(DateTime(d)), values(colorfier)) - dcolorfier = update(colorfier, values=dvalues) - getcolors(dcolorfier) -end +repr(values::AbstractVector{<:DateTime}, colorscheme, colorrange) = + repr(datetime2unix.(values), colorscheme, colorrange) # ----------------- # HELPER FUNCTIONS # ----------------- +isinvalid(value) = ismissing(value) || (value isa Number && !isfinite(value)) + fixcolors(colors) = convert.(floattype(eltype(colors)), colors) -nonmissingvec(x::AbstractVector{T}) where {T} = convert(AbstractVector{nonmissingtype(T)}, x) +nonmissingvec(values::AbstractVector{T}) where {T} = convert(AbstractVector{nonmissingtype(T)}, values) function genvec(vecinds, vec, valinds, val, len) valdict = Dict(i => val for i in valinds) diff --git a/test/Project.toml b/test/Project.toml deleted file mode 100644 index 44e5f85..0000000 --- a/test/Project.toml +++ /dev/null @@ -1,9 +0,0 @@ -[deps] -CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" -ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" -Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/test/runtests.jl b/test/runtests.jl index cbe327e..fbe85a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,224 +9,184 @@ using Dates using Test @testset "Colorfy.jl" begin - @testset "Colorfier" begin + @testset "Handle arguments" begin values = rand(10) - colorfier = Colorfier(values) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colors = [Gray(rand()) for _ in 1:10] - colorfier = Colorfier(colors) - @test eltype(Colorfy.values(colorfier)) <: Gray - @test eltype(eltype(Colorfy.values(colorfier))) <: AbstractFloat - @test Colorfy.values(colorfier) == colors - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colors = [Gray(rand(Q0f7)) for _ in 1:10] - colorfier = Colorfier(colors) - @test eltype(Colorfy.values(colorfier)) <: Gray - @test eltype(eltype(Colorfy.values(colorfier))) <: AbstractFloat - @test Colorfy.values(colorfier) == colors - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colors = [Gray(rand(Q0f15)) for _ in 1:10] - colorfier = Colorfier(colors) - @test eltype(Colorfy.values(colorfier)) <: Gray - @test eltype(eltype(Colorfy.values(colorfier))) <: AbstractFloat - @test Colorfy.values(colorfier) == colors - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colors = [Gray(rand(Q0f31)) for _ in 1:10] - colorfier = Colorfier(colors) - @test eltype(Colorfy.values(colorfier)) <: Gray - @test eltype(eltype(Colorfy.values(colorfier))) <: AbstractFloat - @test Colorfy.values(colorfier) == colors - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colors = [Gray(rand(Q0f63)) for _ in 1:10] - colorfier = Colorfier(colors) - @test eltype(Colorfy.values(colorfier)) <: Gray - @test eltype(eltype(Colorfy.values(colorfier))) <: AbstractFloat - @test Colorfy.values(colorfier) == colors - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colorfier = Colorfier(values, alphas=0.5) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(0.5, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == :extrema - - colorfier = Colorfier(values, colorscheme=:grays) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:grays] - @test Colorfy.colorrange(colorfier) == :extrema - - colorfier = Colorfier(values, colorscheme="grays") - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:grays] - @test Colorfy.colorrange(colorfier) == :extrema - - colorfier = Colorfier(values, colorscheme=["black", "white"]) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier)[0.0] == colorant"black" - @test Colorfy.colorscheme(colorfier)[1.0] == colorant"white" - @test Colorfy.colorrange(colorfier) == :extrema - - colorfier = Colorfier(values, colorrange=(0.25, 0.75)) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == (0.25, 0.75) - - colorfier = Colorfier(values, colorrange=(0, 0.5)) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == fill(1, 10) - @test Colorfy.colorscheme(colorfier) == colorschemes[:viridis] - @test Colorfy.colorrange(colorfier) == (0.0, 0.5) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :viridis, :extrema) + @test vs == values + @test αs == fill(1, 10) + @test s == colorschemes[:viridis] + @test r == :extrema + + values = [Gray(rand()) for _ in 1:10] + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :viridis, :extrema) + @test eltype(vs) <: Gray + @test eltype(eltype(vs)) <: AbstractFloat + @test vs == values + @test αs == fill(1, 10) + @test s == colorschemes[:viridis] + @test r == :extrema + + for Q in (Q0f7, Q0f15, Q0f31, Q0f63) + values = [Gray(rand(Q)) for _ in 1:10] + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :viridis, :extrema) + @test eltype(vs) <: Gray + @test eltype(eltype(vs)) <: AbstractFloat + @test vs == values + @test αs == fill(1, 10) + @test s == colorschemes[:viridis] + @test r == :extrema + end + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 0.5, :viridis, :extrema) + @test αs == fill(0.5, 10) + + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :grays, :extrema) + @test s == colorschemes[:grays] + + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, "grays", :extrema) + @test s == colorschemes[:grays] + + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, ["black", "white"], :extrema) + @test s[0.0] == colorant"black" + @test s[1.0] == colorant"white" + + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :viridis, (0.25, 0.75)) + @test r == (0.25, 0.75) + + values = rand(10) + vs, αs, s, r = Colorfy.handleargs(values, 1.0, :viridis, (0, 0.5)) + @test r == (0.0, 0.5) + + values = rand(10) alphas = rand(10) - colorfier = Colorfier(values; alphas, colorscheme=colorschemes[:grays], colorrange=(0.25, 0.75)) - @test Colorfy.values(colorfier) == values - @test Colorfy.alphas(colorfier) == alphas - @test Colorfy.colorscheme(colorfier) == colorschemes[:grays] - @test Colorfy.colorrange(colorfier) == (0.25, 0.75) + vs, αs, s, r = Colorfy.handleargs(values, alphas, colorschemes[:grays], (0.25, 0.75)) + @test vs == values + @test αs == alphas + @test s == colorschemes[:grays] + @test r == (0.25, 0.75) # error: the number of alphas must be equal to the number of values + values = rand(10) alphas = rand(9) - @test_throws ArgumentError Colorfier(values; alphas) + @test_throws ArgumentError Colorfy.handleargs(values, alphas, :viridis, :extrema) end - @testset "Colorfy.colors" begin + @testset "Basic tests" begin values = rand(10) - colorfier = Colorfier(values) - colors = get(colorschemes[:viridis], values, :extrema) - @test Colorfy.colors(colorfier) == coloralpha.(colors, 1) + colors = colorfy(values) + result = get(colorschemes[:viridis], values, :extrema) + @test colors == coloralpha.(result, 1) - colorfier = Colorfier(values, colorscheme=:grays, colorrange=(0.25, 0.75)) - colors = get(colorschemes[:grays], values, (0.25, 0.75)) - @test Colorfy.colors(colorfier) == coloralpha.(colors, 1) + values = rand(10) + colors = colorfy(values, colorscheme=:grays, colorrange=(0.25, 0.75)) + result = get(colorschemes[:grays], values, (0.25, 0.75)) + @test colors == coloralpha.(result, 1) - colors = [colorant"red", colorant"green", colorant"blue", colorant"white", colorant"black"] values = ["red", "green", "blue", "white", "black"] - colorfier = Colorfier(values) - @test Colorfy.colors(colorfier) == coloralpha.(colors, 1) + colors = colorfy(values) + result = [colorant"red", colorant"green", colorant"blue", colorant"white", colorant"black"] + @test colors == coloralpha.(result, 1) values = [:red, :green, :blue, :white, :black] - colorfier = Colorfier(values, alphas=0.5) - @test Colorfy.colors(colorfier) == coloralpha.(colors, 0.5) + colors1 = colorfy(values) + colors2 = colorfy(values, alpha=0.5) + @test colors2 == coloralpha.(colors1, 0.5) - values = colors + values = [:red, :green, :blue, :white, :black] alphas = rand(5) - colorfier = Colorfier(values; alphas) - @test Colorfy.colors(colorfier) == coloralpha.(colors, alphas) - - values = coloralpha.(colors, alphas) - colorfier = Colorfier(values) - @test Colorfy.colors(colorfier) == values + colors1 = colorfy(values) + colors2 = colorfy(values, alpha=alphas) + @test colors2 == coloralpha.(colors1, alphas) - # error: unsupported values - values = [nothing, nothing, nothing] - colorfier = Colorfier(values) - @test_throws ArgumentError Colorfy.colors(colorfier) - values = Any[:red, :green, :blue] # vector with non-concrete eltype - colorfier = Colorfier(values) - @test_throws ArgumentError Colorfy.colors(colorfier) - end - - @testset "colorfy" begin - values = rand(10) - colors = get(colorschemes[:viridis], values, :extrema) - @test colorfy(values) == coloralpha.(colors, 1) - - colors = get(colorschemes[:grays], values, (0.25, 0.75)) - @test colorfy(values, colorscheme=:grays, colorrange=(0.25, 0.75)) == coloralpha.(colors, 1) - end - - @testset "Invalid values" begin + # invalid values (missing, NaN, Inf) are made transparent values = [0.1, missing, 0.2, NaN, 0.3, Inf, 0.4, -Inf, 0.5] - colors = colorfy(values, alphas=0.5) + colors = colorfy(values, alpha=0.5) @test colors[2] == colorant"transparent" @test colors[4] == colorant"transparent" @test colors[6] == colorant"transparent" @test colors[8] == colorant"transparent" - # Vector{Union{Missing,T}} whitout missing values + # if all values are invalid, return transparent colors + values = [missing, NaN, Inf, -Inf] + colors = colorfy(values) + @test all(c -> c == colorant"transparent", colors) + values = [missing, missing, missing, missing] + colors = colorfy(values) + @test all(c -> c == colorant"transparent", colors) + + # Vector{Union{Missing,T}} without missing values values = Union{Missing,Int}[1, 2, 3, 4, 5] @test colorfy(values) == colorfy([1, 2, 3, 4, 5]) + + # error: unsupported values + values = [nothing, nothing, nothing] + @test_throws ArgumentError colorfy(values) + values = Any[:red, :green, :blue] # vector with non-concrete eltype + @test_throws ArgumentError colorfy(values) end @testset "Dates" begin values = now() .+ Day.(1:10) - colors = colorfy(datetime2unix.(values)) - @test colorfy(values) == colors - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + colors1 = colorfy(values) + colors2 = colorfy(datetime2unix.(values)) + @test colors1 == colors2 + @test colorfy(values, alpha=0.5) == coloralpha.(colors1, 0.5) values = today() .+ Day.(1:10) - colors = colorfy(datetime2unix.(DateTime.(values))) - @test colorfy(values) == colors - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + colors1 = colorfy(values) + colors2 = colorfy(DateTime.(values)) + @test colors1 == colors2 + @test colorfy(values, alpha=0.5) == coloralpha.(colors1, 0.5) end @testset "CategoricalArrays" begin values = categorical(["n", "n", "y", "y", "n", "y"], levels=["y", "n"]) - categcolors = colorschemes[:viridis][range(0, 1, length=2)] - colors = categcolors[[2, 2, 1, 1, 2, 1]] - @test colorfy(values) == coloralpha.(colors, 1) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + lcolors = colorschemes[:viridis][range(0, 1, length=2)] + result = lcolors[[2, 2, 1, 1, 2, 1]] + @test colorfy(values) == coloralpha.(result, 1) + @test colorfy(values, alpha=0.5) == coloralpha.(result, 0.5) values = categorical([2, 1, 1, 3, 1, 3, 3, 2, 1, 2], levels=1:3) - categcolors = colorschemes[:viridis][range(0, 1, length=3)] - colors = categcolors[[2, 1, 1, 3, 1, 3, 3, 2, 1, 2]] - @test colorfy(values) == coloralpha.(colors, 1) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + lcolors = colorschemes[:viridis][range(0, 1, length=3)] + result = lcolors[[2, 1, 1, 3, 1, 3, 3, 2, 1, 2]] + @test colorfy(values) == coloralpha.(result, 1) + @test colorfy(values, alpha=0.5) == coloralpha.(result, 0.5) values = categorical([1, 1, 1, 1, 1], levels=[1]) - categcolors = colorschemes[:viridis][range(0, 0, length=1)] - colors = categcolors[[1, 1, 1, 1, 1]] - @test colorfy(values) == coloralpha.(colors, 1) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + lcolors = colorschemes[:viridis][range(0, 0, length=1)] + result = lcolors[[1, 1, 1, 1, 1]] + @test colorfy(values) == coloralpha.(result, 1) + @test colorfy(values, alpha=0.5) == coloralpha.(result, 0.5) end @testset "Distributions" begin - means = rand(10) - stds = rand(10) - values = Normal.(means, stds) - alphas = 1 .- (stds .- minimum(stds)) ./ (maximum(stds) - minimum(stds)) - colors = colorfy(means) - @test colorfy(values) == coloralpha.(colors, alphas) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) - - values = Normal.(means, fill(0.5, 10)) - @test colorfy(values) == coloralpha.(colors, 1) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, 0.5) + values = Normal.(rand(10), rand(10)) + μs = location.(values) + σs = scale.(values) + a, b = extrema(σs) + colors = colorfy(μs, alpha=1 .- (σs .- a) ./ (b .- a)) + alphas = map(Colors.alpha, colors) + @test colorfy(values) == colors + @test colorfy(values, alpha=0.5) == coloralpha.(colors, 0.5 * alphas) values = [missing, Normal(0.5, 0.5), Normal(0.6, 0.6), Normal(0.7, 0.7), missing] - colors = [colorant"transparent"; colorfy([0.5, 0.6, 0.7]); colorant"transparent"] - alphas = [0.0, 1.0, 0.5, 0.0, 0.0] colors = colorfy(values) - @test colorfy(values) == coloralpha.(colors, alphas) - @test colorfy(values, alphas=0.5) == coloralpha.(colors, [0.0, 0.5, 0.5, 0.5, 0.0]) + @test colors[1] == colorant"transparent" + @test colors[2] != colorant"transparent" + @test colors[3] != colorant"transparent" + @test colors[4] != colorant"transparent" + @test colors[5] == colorant"transparent" end @testset "Unitful" begin values = rand(10) * u"m" @test colorfy(values) == colorfy(ustrip.(values)) - @test colorfy(values, alphas=0.5) == colorfy(ustrip.(values), alphas=0.5) + @test colorfy(values, alpha=0.5) == colorfy(ustrip.(values), alpha=0.5) @test colorfy(values, colorrange=(0.25, 0.75)) == colorfy(ustrip.(values), colorrange=(0.25, 0.75)) @test colorfy(values, colorrange=(0.25u"m", 0.75u"m")) == colorfy(ustrip.(values), colorrange=(0.25, 0.75)) end