Skip to content

Commit

Permalink
add and export ReversibleScale type - simplify LogFunctions (#3095)
Browse files Browse the repository at this point in the history
* add `ReversibleScale` type - simplify `LogFunctions`

* fix `scaled_steps`

* add news entry

* add more flexibility

* simplify `interval` kw

* add docstring

* apply sugestions - clarify warnings

* remove space

* simplify `colorbar_range`

* add inverse transform checks

* renames

* drop `LogTicks` - rework `Symlog10` and `pseudolog10`

* add limits

* remove test

* export `ReversibleScale`

* add `ReversibleScale` docs to `heatmap`

* update `contour` docs

* enhance docs example

---------

Co-authored-by: Simon <sdanisch@protonmail.com>
Co-authored-by: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 8, 2023
1 parent 7ebe960 commit 03293c6
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 85 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# News

## master
- Allow arbitrary reversible scale functions through `ReversibleScale`.

- Deprecated `linestyle=vector_of_gaps` in favor of `linestyle=Linestyle(vector_of_gaps)` [3135](https://github.com/MakieOrg/Makie.jl/pull/3135), [3193](https://github.com/MakieOrg/Makie.jl/pull/3193).
- Fixed some errors around dynamic changes of `ax.xscale` or `ax.yscale` [#3084](https://github.com/MakieOrg/Makie.jl/pull/3084)
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/plots/contour.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ x = y = range(-6, 6; length=100)
z = himmelblau.(x, y')

levels = 10.0.^range(0.3, 3.5; length=10)
colormap = Makie.sampler(:hsv, 100; scaling=Makie.Scaling(x -> x^(1 / 10), nothing))
f, ax, ct = contour(x, y, z; labels=true, levels, colormap)
colorscale = ReversibleScale(x -> x^(1 / 10), x -> x^10)
f, ax, ct = contour(x, y, z; labels=true, levels, colormap=:hsv, colorscale)
f
```
\end{examplefigure}
22 changes: 22 additions & 0 deletions docs/reference/plots/heatmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,25 @@ Colorbar(fig[:, end+1], colorrange = joint_limits) # equivalent
fig
```
\end{examplefigure}


### Using a custom colorscale

One can define a custom (color)scale using the `ReversibleScale` type. When the transformation is simple enough (`log`, `sqrt`, ...), the inverse transform is automatically deduced.

\begin{examplefigure}{}
```julia
using CairoMakie
CairoMakie.activate!() # hide

x = 10.0.^(1:0.1:4)
y = 1.0:0.1:5.0
z = broadcast((x, y) -> x - 10, x, y')

scale = ReversibleScale(x -> asinh(x / 2) / log(10), x -> 2sinh(log(10) * x))
fig, ax, hm = heatmap(x, y, z; colorscale = scale, axis = (; xscale = scale))
Colorbar(fig[1, 2], hm)

fig
```
\end{examplefigure}
1 change: 1 addition & 0 deletions src/Makie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export save, colorbuffer
export cgrad, available_gradients, showgradients

export Pattern
export ReversibleScale

export assetpath
# default icon for Makie
Expand Down
4 changes: 2 additions & 2 deletions src/colorsampler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ end
struct ColorMap{N,T<:AbstractArray{<:Number,N},T2<:AbstractArray{<:Number,N}}
color::Observable{T}
colormap::Observable{Vector{RGBAf}}
scale::Observable{Function}
scale::Observable{Union{ReversibleScale, Function}}
mapping::Observable{Union{Nothing, Vector{Float64}}}
colorrange::Observable{Vec{2,Float64}}

Expand All @@ -198,7 +198,7 @@ function assemble_colors(::T, @nospecialize(color), @nospecialize(plot)) where {
color_tight = convert(Observable{T}, color)
colormap = Observable(RGBAf[]; ignore_equal_values=true)
categorical = Observable(false)
colorscale = convert(Observable{Function}, plot.colorscale)
colorscale = convert(Observable{Union{ReversibleScale, Function}}, plot.colorscale)
mapping = Observable{Union{Nothing, Vector{Float64}}}(nothing)

function update_colors(cmap, a)
Expand Down
63 changes: 19 additions & 44 deletions src/layouting/transformation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,6 @@ function apply_transform(f, itr::ClosedInterval)
return apply_transform(f, mini) .. apply_transform(f, maxi)
end


function apply_transform(f, r::Rect)
mi = minimum(r)
ma = maximum(r)
Expand All @@ -360,66 +359,42 @@ apply_transform(f::typeof(identity), r::Rect) = r
apply_transform(f::NTuple{2, typeof(identity)}, r::Rect) = r
apply_transform(f::NTuple{3, typeof(identity)}, r::Rect) = r

const pseudolog10 = ReversibleScale(
x -> sign(x) * log10(abs(x) + 1),
x -> sign(x) * (exp10(abs(x)) - 1);
limits=(0f0, 3f0)
)

pseudolog10(x) = sign(x) * log10(abs(x) + 1)
inv_pseudolog10(x) = sign(x) * (exp10(abs(x)) - 1)

struct Symlog10
low::Float64
high::Float64
function Symlog10(low, high)
if !(low < 0 && high > 0)
error("Low bound needs to be smaller than 0 and high bound larger than 0. You gave $low, $high.")
end
new(Float64(low), Float64(high))
end
end

Symlog10(x) = Symlog10(-x, x)

function (s::Symlog10)(x)
if x > 0
x <= s.high ? x / s.high * log10(s.high) : log10(x)
Symlog10(hi) = Symlog10(-hi, hi)
function Symlog10(lo, hi)
forward(x) = if x > 0
x <= hi ? x / hi * log10(hi) : log10(x)
elseif x < 0
x >= s.low ? x / abs(s.low) * log10(abs(s.low)) : sign(x) * log10(abs(x))
x >= lo ? x / abs(lo) * log10(abs(lo)) : -log10(abs(x))
else
x
end
end

function inv_symlog10(x, low, high)
if x > 0
l = log10(high)
x <= l ? x / l * high : exp10(x)
inverse(x) = if x > 0
l = log10(hi)
x <= l ? x / l * hi : exp10(x)
elseif x < 0
l = sign(x) * log10(abs(low))
x >= l ? x / l * abs(low) : sign(x) * exp10(abs(x))
l = -log10(abs(lo))
x >= l ? x / l * abs(lo) : -exp10(abs(x))
else
x
end
ReversibleScale(forward, inverse; limits=(0f0, 3f0))
end

const REVERSIBLE_SCALES = Union{
# typeof(identity), # no, this is a noop
typeof(log10),
typeof(log),
typeof(log2),
typeof(sqrt),
typeof(pseudolog10),
typeof(logit),
Symlog10,
}

inverse_transform(::typeof(identity)) = identity
inverse_transform(::typeof(log10)) = exp10
inverse_transform(::typeof(log)) = exp
inverse_transform(::typeof(log2)) = exp2
inverse_transform(::typeof(log)) = exp
inverse_transform(::typeof(sqrt)) = x -> x ^ 2
inverse_transform(::typeof(pseudolog10)) = inv_pseudolog10
inverse_transform(F::Tuple) = map(inverse_transform, F)
inverse_transform(::typeof(logit)) = logistic
inverse_transform(s::Symlog10) = x -> inv_symlog10(x, s.low, s.high)
inverse_transform(s) = nothing
inverse_transform(s::ReversibleScale) = s.inverse
inverse_transform(::Any) = nothing

function is_identity_transform(t)
return t === identity || t isa Tuple && all(x-> x === identity, t)
Expand Down
17 changes: 7 additions & 10 deletions src/makielayout/blocks/axis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ function initialize_block!(ax::Axis; palette = nothing)
register_events!(ax, scene)

# these are the user defined limits
on(blockscene, ax.limits) do mlims
on(blockscene, ax.limits) do _
reset_limits!(ax)
end

Expand Down Expand Up @@ -1352,22 +1352,19 @@ defaultlimits(limits::Tuple{Real, Nothing}, scale) = (limits[1], defaultlimits(s
defaultlimits(limits::Tuple{Nothing, Real}, scale) = (defaultlimits(scale)[1], limits[2])
defaultlimits(limits::Tuple{Nothing, Nothing}, scale) = defaultlimits(scale)


defaultlimits(::typeof(log10)) = (1.0, 1000.0)
defaultlimits(::typeof(log2)) = (1.0, 8.0)
defaultlimits(::typeof(log)) = (1.0, exp(3.0))
defaultlimits(scale::ReversibleScale) = inverse_transform(scale).(scale.limits)
defaultlimits(scale::LogFunctions) = let inv_scale = inverse_transform(scale)
(inv_scale(0.0), inv_scale(3.0))
end
defaultlimits(::typeof(identity)) = (0.0, 10.0)
defaultlimits(::typeof(sqrt)) = (0.0, 100.0)
defaultlimits(::typeof(Makie.logit)) = (0.01, 0.99)
defaultlimits(::typeof(Makie.pseudolog10)) = (0.0, 100.0)
defaultlimits(::Makie.Symlog10) = (0.0, 100.0)

defined_interval(scale::ReversibleScale) = scale.interval
defined_interval(::typeof(identity)) = OpenInterval(-Inf, Inf)
defined_interval(::Union{typeof(log2), typeof(log10), typeof(log)}) = OpenInterval(0.0, Inf)
defined_interval(::LogFunctions) = OpenInterval(0.0, Inf)
defined_interval(::typeof(sqrt)) = Interval{:closed,:open}(0, Inf)
defined_interval(::typeof(Makie.logit)) = OpenInterval(0.0, 1.0)
defined_interval(::typeof(Makie.pseudolog10)) = OpenInterval(-Inf, Inf)
defined_interval(::Makie.Symlog10) = OpenInterval(-Inf, Inf)

function update_state_before_display!(ax::Axis)
reset_limits!(ax)
Expand Down
28 changes: 15 additions & 13 deletions src/makielayout/blocks/colorbar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,15 @@ function Colorbar(fig_or_scene, voronoi::Voronoiplot; kwargs...)
)
end

colorbar_range(start, stop, length, _) = LinRange(start, stop, length) # noop
function colorbar_range(start, stop, length, scale::REVERSIBLE_SCALES)
inverse_transform(scale).(range(start, stop; length))
function colorbar_range(start, stop, length, colorscale)
colorscale === identity && return LinRange(start, stop, length)

inverse = inverse_transform(colorscale)
isnothing(inverse) && throw(ArgumentError(
"Cannot determine inverse transform: you can use `ReversibleScale($(colorscale), inverse($(colorscale)))` instead."
))

inverse.(range(start, stop; length))
end

function initialize_block!(cb::Colorbar)
Expand Down Expand Up @@ -168,7 +174,7 @@ function initialize_block!(cb::Colorbar)
map_is_categorical = lift(x -> x isa PlotUtils.CategoricalColorGradient, blockscene, cgradient)

steps = lift(blockscene, cgradient, cb.nsteps, cb.scale) do cgradient, n, scale
s = if cgradient isa PlotUtils.CategoricalColorGradient
if cgradient isa PlotUtils.CategoricalColorGradient
cgradient.values
else
collect(colorbar_range(0, 1, n, scale))
Expand Down Expand Up @@ -385,17 +391,13 @@ end
Sets the space allocated for the ticklabels of the `Colorbar` to the minimum that is needed and returns that value.
"""
function tight_ticklabel_spacing!(cb::Colorbar)
space = tight_ticklabel_spacing!(cb.axis)
return space
end
tight_ticklabel_spacing!(cb::Colorbar) = tight_ticklabel_spacing!(cb.axis)

function scaled_steps(steps, scale, lims)
# first scale to limits so we can actually apply the scale to the values
# (log(0) doesn't work etc.)
s_limits = steps .* (lims[2] - lims[1]) .+ lims[1]
# scale with scaling function
s_limits_scaled = scale.(s_limits)
steps_scaled = scale.(steps)
# normalize to lims range
steps_lim_scaled = @. steps_scaled * (scale(lims[2]) - scale(lims[1])) + scale(lims[1])
# then rescale to 0 to 1
s_scaled = (s_limits_scaled .- s_limits_scaled[1]) ./ (s_limits_scaled[end] - s_limits_scaled[1])
@. (steps_lim_scaled - steps_lim_scaled[begin]) / (steps_lim_scaled[end] - steps_lim_scaled[begin])
end
23 changes: 10 additions & 13 deletions src/makielayout/lineaxis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,12 @@ end
get_tickvalues(::Automatic, ::typeof(identity), vmin, vmax) = get_tickvalues(WilkinsonTicks(5, k_min = 3), vmin, vmax)

# fall back to identity if not overloaded scale function is used with automatic
get_tickvalues(::Automatic, F, vmin, vmax) = get_tickvalues(automatic, identity, vmin, vmax)
get_tickvalues(::Automatic, _, vmin, vmax) = get_tickvalues(automatic, identity, vmin, vmax)

# fall back to non-scale aware behavior if no special version is overloaded
get_tickvalues(ticks, scale, vmin, vmax) = get_tickvalues(ticks, vmin, vmax)
get_tickvalues(ticks, _, vmin, vmax) = get_tickvalues(ticks, vmin, vmax)

function get_ticks(ticks_and_labels::Tuple{Any, Any}, any_scale, ::Automatic, vmin, vmax)
function get_ticks(ticks_and_labels::Tuple{Any, Any}, _, ::Automatic, vmin, vmax)
n1 = length(ticks_and_labels[1])
n2 = length(ticks_and_labels[2])
if n1 != n2
Expand All @@ -570,7 +570,7 @@ function get_ticks(ticks_and_labels::Tuple{Any, Any}, any_scale, ::Automatic, vm
ticks_and_labels
end

function get_ticks(tickfunction::Function, any_scale, formatter, vmin, vmax)
function get_ticks(tickfunction::Function, _, formatter, vmin, vmax)
result = tickfunction(vmin, vmax)
if result isa Tuple{Any, Any}
tickvalues, ticklabels = result
Expand All @@ -585,14 +585,13 @@ _logbase(::typeof(log10)) = "10"
_logbase(::typeof(log2)) = "2"
_logbase(::typeof(log)) = "e"


function get_ticks(::Automatic, scale::Union{typeof(log10), typeof(log2), typeof(log)},
any_formatter, vmin, vmax)
get_ticks(LogTicks(WilkinsonTicks(5, k_min = 3)), scale, any_formatter, vmin, vmax)
function get_ticks(::Automatic, scale::LogFunctions, any_formatter, vmin, vmax)
ticks = LogTicks(WilkinsonTicks(5, k_min = 3))
get_ticks(ticks, scale, any_formatter, vmin, vmax)
end

# log ticks just use the normal pipeline but with log'd limits, then transform the labels
function get_ticks(l::LogTicks, scale::Union{typeof(log10), typeof(log2), typeof(log)}, ::Automatic, vmin, vmax)
function get_ticks(l::LogTicks, scale::LogFunctions, ::Automatic, vmin, vmax)
ticks_scaled = get_tickvalues(l.linear_ticks, identity, scale(vmin), scale(vmax))

ticks = Makie.inverse_transform(scale).(ticks_scaled)
Expand All @@ -605,7 +604,7 @@ function get_ticks(l::LogTicks, scale::Union{typeof(log10), typeof(log2), typeof
)
labels = rich.(_logbase(scale), superscript.(labels_scaled, offset = Vec2f(0.1f0, 0f0)))

(ticks, labels)
ticks, labels
end

# function get_ticks(::Automatic, scale::typeof(Makie.logit), any_formatter, vmin, vmax)
Expand Down Expand Up @@ -684,7 +683,6 @@ Gets tick labels by formatting each value in `values` according to a `Formatting
"""
get_ticklabels(formatstring::AbstractString, values) = [Formatting.format(formatstring, v) for v in values]


function get_ticks(m::MultiplesTicks, any_scale, ::Automatic, vmin, vmax)
dvmin = vmin / m.multiple
dvmax = vmax / m.multiple
Expand Down Expand Up @@ -727,8 +725,7 @@ function get_minor_tickvalues(i::IntervalsBetween, scale, tickvalues, vmin, vmax
end

# for log scales, we need to step in log steps at the edges
function get_minor_tickvalues(i::IntervalsBetween, scale::Union{typeof(log),typeof(log2),typeof(log10)},
tickvalues, vmin, vmax)
function get_minor_tickvalues(i::IntervalsBetween, scale::LogFunctions, tickvalues, vmin, vmax)
vals = Float64[]
length(tickvalues) < 2 && return vals
n = i.n
Expand Down
51 changes: 51 additions & 0 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ end
Struct to hold all relevant matrices and additional parameters, to let backends
apply camera based transformations.
## Fields
$(TYPEDFIELDS)
"""
struct Camera
"""
Expand Down Expand Up @@ -378,3 +381,51 @@ end

# The color type we ideally use for most color attributes
const RGBColors = Union{RGBAf, Vector{RGBAf}, Vector{Float32}}

const LogFunctions = Union{typeof(log10), typeof(log2), typeof(log)}

"""
ReversibleScale
Custom scale struct, taking a forward and inverse arbitrary scale function.
## Fields
$(TYPEDFIELDS)
"""
struct ReversibleScale{F <: Function, I <: Function, T <: AbstractInterval}
"""
forward transformation (e.g. `log10`)
"""
forward::F
"""
inverse transformation (e.g. `exp10` for `log10` such that inverse ∘ forward ≡ identity)
"""
inverse::I
"""
default limits (optional)
"""
limits::NTuple{2,Float32}
"""
valid limits interval (optional)
"""
interval::T
function ReversibleScale(forward, inverse = Automatic(); limits = (0f0, 10f0), interval = (-Inf32, Inf32))
inverse isa Automatic && (inverse = inverse_transform(forward))
isnothing(inverse) && throw(ArgumentError(
"Cannot determine inverse transform: you can use `ReversibleScale($(forward), inverse($(forward)))` instead."
))
interval isa AbstractInterval || (interval = OpenInterval(Float32.(interval)...))

lft, rgt = limits = Tuple(Float32.(limits))

Id = inverse forward
lft Id(lft) || throw(ArgumentError("Invalid inverse transform: $lft !≈ $(Id(lft))"))
rgt Id(rgt) || throw(ArgumentError("Invalid inverse transform: $rgt !≈ $(Id(rgt))"))

new{typeof(forward),typeof(inverse),typeof(interval)}(forward, inverse, limits, interval)
end
end

function (s::ReversibleScale)(args...) # functor
s.forward(args...)
end
Loading

0 comments on commit 03293c6

Please sign in to comment.