Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add and export ReversibleScale type - simplify LogFunctions #3095

Merged
merged 32 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cac175b
add `ReversibleScale` type - simplify `LogFunctions`
t-bltg Jul 25, 2023
f26d816
fix `scaled_steps`
t-bltg Jul 25, 2023
bed5823
add news entry
t-bltg Jul 25, 2023
56d597a
add more flexibility
t-bltg Aug 8, 2023
ee9f3df
Merge branch 'master' into cscale-overlap
t-bltg Aug 8, 2023
d0de587
Merge branch 'master' into cscale-overlap
t-bltg Aug 11, 2023
ec9ed84
simplify `interval` kw
t-bltg Aug 11, 2023
93a4af3
add docstring
t-bltg Aug 11, 2023
82c68d4
apply sugestions - clarify warnings
t-bltg Aug 20, 2023
1a7e0f5
Merge branch 'master' into cscale-overlap
t-bltg Aug 20, 2023
4ec2f98
remove space
t-bltg Aug 20, 2023
a4bab43
Merge branch 'master' into cscale-overlap
SimonDanisch Aug 21, 2023
14706cd
Merge branch 'master' into cscale-overlap
SimonDanisch Aug 22, 2023
7befef1
simplify `colorbar_range`
t-bltg Aug 22, 2023
70113ca
add inverse transform checks
t-bltg Aug 23, 2023
17454b5
renames
t-bltg Aug 23, 2023
4629045
Merge branch 'master' into cscale-overlap
t-bltg Aug 25, 2023
ef8c387
drop `LogTicks` - rework `Symlog10` and `pseudolog10`
t-bltg Aug 27, 2023
512d9f4
add limits
t-bltg Aug 27, 2023
7744883
remove test
t-bltg Aug 27, 2023
5db8693
Merge branch 'master' into cscale-overlap
SimonDanisch Aug 29, 2023
254f0a2
export `ReversibleScale`
t-bltg Aug 29, 2023
2e6c03d
add `ReversibleScale` docs to `heatmap`
t-bltg Aug 29, 2023
fbf6e5a
Merge branch 'master' into cscale-overlap
t-bltg Aug 30, 2023
8fa493f
Merge branch 'master' into cscale-overlap
t-bltg Aug 30, 2023
8ff6e83
Merge branch 'master' into cscale-overlap
t-bltg Aug 30, 2023
68e5248
Merge branch 'master' into cscale-overlap
SimonDanisch Aug 30, 2023
ae99f7e
Merge branch 'master' into cscale-overlap
jkrumbiegel Aug 31, 2023
ae544e2
Merge branch 'master' into cscale-overlap
jkrumbiegel Aug 31, 2023
29f21bd
update `contour` docs
t-bltg Sep 5, 2023
4cd096b
Merge branch 'master' into cscale-overlap
t-bltg Sep 5, 2023
07b26b0
enhance docs example
t-bltg Sep 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.

- Fixed some errors around dynamic changes of `ax.xscale` or `ax.yscale` [#3084](https://github.com/MakieOrg/Makie.jl/pull/3084)
- Improved Barplot Label Alignment [#3160](https://github.com/MakieOrg/Makie.jl/issues/3160).
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))
t-bltg marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -287,6 +287,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
Loading