-
Notifications
You must be signed in to change notification settings - Fork 24
Basic Makie.jl support #165
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
base: main
Are you sure you want to change the base?
Changes from all commits
84a6d8a
e6dc26a
2eb116f
bbf9eff
5b7920f
3362969
e439270
e68d3b0
e1637d3
8dcc31e
331d8c6
6492347
b307dd8
43f95ab
90c5d46
7d83d1b
66f8899
8336dc5
1c5127e
2893c01
d412ed6
1277f6f
1afe364
2d09a66
ef2d0d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
[deps] | ||
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" | ||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" | ||
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,53 +40,58 @@ Also, using `DynamicQuantities.Constants`, we were able to obtain the (dimension | |
Let's solve a simple projectile motion problem. | ||
First load the `DynamicQuantities` module: | ||
|
||
```julia | ||
```@example projectile | ||
using DynamicQuantities | ||
``` | ||
|
||
Set up initial conditions as quantities: | ||
|
||
```julia | ||
```@example projectile | ||
# Can explicitly import units: | ||
using DynamicQuantities: km, m, s, min | ||
y0 = 10km | ||
v0 = 250m/s | ||
θ = deg2rad(60) | ||
g = 9.81m/s^2 | ||
nothing # hide | ||
``` | ||
|
||
Next, we use trig functions to calculate x and y components of initial velocity. | ||
`vx0` is the x component and | ||
`vy0` is the y component: | ||
|
||
```julia | ||
```@example projectile | ||
vx0 = v0 * cos(θ) | ||
vy0 = v0 * sin(θ) | ||
nothing # hide | ||
``` | ||
|
||
Next, let's create a time vector from 0 seconds to 1.3 minutes. | ||
Note that these are the same dimension (time), so it's fine to treat | ||
them as dimensionally equivalent! | ||
|
||
```julia | ||
```@example projectile | ||
t = range(0s, 1.3min, length=100) | ||
nothing # hide | ||
``` | ||
|
||
Next, use kinematic equations to calculate x and y as a function of time. | ||
`x(t)` is the x position at time t, and | ||
`y(t)` is the y position: | ||
|
||
```julia | ||
```@example projectile | ||
x(t) = vx0*t | ||
y(t) = vy0*t - 0.5*g*t^2 + y0 | ||
nothing # hide | ||
``` | ||
|
||
These are functions, so let's evaluate them: | ||
|
||
```julia | ||
```@example projectile | ||
x_si = x.(t) | ||
y_si = y.(t) | ||
nothing # hide | ||
``` | ||
|
||
These are regular vectors of quantities | ||
|
@@ -106,6 +111,8 @@ Now, we plot: | |
plot(x_km, y_km, label="Trajectory", xlabel="x [km]", ylabel="y [km]") | ||
``` | ||
|
||
See [Plotting](@ref) for more plotting support with units. | ||
|
||
## 3. Using dimensional angles | ||
|
||
Say that we wish to track angles as a unit, rather than assume | ||
|
@@ -504,3 +511,60 @@ function my_func(x::UnionAbstractQuantity{T,D}) where {T,D} | |
return x / ustrip(x) | ||
end | ||
``` | ||
|
||
### Plotting | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need an explicit example of this? i.e., is it something that users would expect to "just work" anyways? |
||
|
||
We can use [`Makie.jl`](https://docs.makie.org/v0.22/) to create plots with units. Below are a few usage examples. See [Makie.jl > Dimension conversion](https://docs.makie.org/stable/explanations/dim-converts#Current-conversions-in-Makie) for more. | ||
|
||
!!! warning "Experimental" | ||
Unit support is still a new feature, so please report an issue if you notice any unintended behavior. | ||
|
||
Continuing from [2. Projectile motion](@ref), we can also plot `x_si` and `y_si` directly without needing to manually strip their units beforehand: | ||
|
||
```@example projectile | ||
using CairoMakie | ||
lines(x_si, y_si; axis=(xlabel="x", ylabel="y")) | ||
``` | ||
|
||
To convert units, we pass a `DQConversion` object to `axis` with our desired unit: | ||
|
||
```@example projectile | ||
# Temporary until this is upstreamed to Makie.jl | ||
const DQConversion = Base.get_extension(DynamicQuantities, :DynamicQuantitiesMakieExt).DQConversion | ||
lines(x_si, y_si; | ||
axis = ( | ||
xlabel = "x", | ||
ylabel = "y", | ||
dim1_conversion = DQConversion(us"km"), | ||
dim2_conversion = DQConversion(us"km"), | ||
) | ||
) | ||
``` | ||
|
||
!!! note | ||
A [Symbolic Dimensions](@ref) object is passed to `DQConversion` for this conversion to work properly. Passing a regular `Quantity`, e.g., (u"km") will throw an error. | ||
|
||
Finally, the desired units for a figure can also be set ahead of time. All plot objects within it will automatically convert to the given units: | ||
|
||
```@example projectile | ||
fig = Figure() | ||
ax = Axis(fig[1, 1]; | ||
xlabel = "time", | ||
ylabel = "displacement", | ||
dim1_conversion=DQConversion(us"s"), | ||
dim2_conversion=DQConversion(us"km"), | ||
) | ||
lines!(ax, t, x_si; label="x") | ||
lines!(ax, t, y_si; label="y") | ||
axislegend() | ||
fig | ||
``` | ||
|
||
!!! tip | ||
For nice unit support with Makie.jl and tabular data, see [AlgebraOfGraphics.jl > Units](https://aog.makie.org/dev/examples/scales/units#units). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
module DynamicQuantitiesMakieExt | ||
|
||
using DynamicQuantities: UnionAbstractQuantity, SymbolicDimensions, ustrip, dimension | ||
using TestItems: @testitem | ||
|
||
import Makie as M | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just out of curiosity, is there a package higher up the plotting tools hierarchy that we should be overloading instead? For example there is RecipesBase.jl. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make MakieCore.jl? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You know, the distinction between MakieCore and Makie proper when it comes to defining recipes and axis conversions is something that I've been trying to clarify for myself too AFAICT, MakieCore is for recipes, while axis converts (like in this PR) require the whole megillah so things like Interestingly, it looks like the long term plan may be to phase out MakieCore, but idk what the latest on that is MakieOrg/Makie.jl#3645 (comment) |
||
|
||
M.expand_dimensions(::M.PointBased, y::AbstractVector{<:UnionAbstractQuantity}) = (keys(y), y) | ||
M.create_dim_conversion(::Type{<:UnionAbstractQuantity}) = DQConversion() | ||
M.MakieCore.should_dim_convert(::Type{<:UnionAbstractQuantity}) = true | ||
|
||
unit_string(quantity::UnionAbstractQuantity) = string(dimension(quantity)) | ||
|
||
function unit_convert(::M.Automatic, x) | ||
x | ||
end | ||
|
||
function unit_convert(quantity::UnionAbstractQuantity, x::AbstractArray) | ||
# Note: unit_convert.(Ref(quantity), x) currently causes broadcasting error for `QuantityArray`s | ||
map(Base.Fix1(unit_convert, quantity), x) | ||
end | ||
|
||
function unit_convert(quantity::UnionAbstractQuantity, value) | ||
conv = ustrip(quantity, value) | ||
return Float64(conv) | ||
end | ||
|
||
""" | ||
DQConversion(unit=automatic; units_in_label=false) | ||
|
||
Allows to plot arrays of DynamicQuantity objects into an axis. | ||
|
||
# Arguments | ||
- `unit=automatic`: sets the unit as conversion target. If left at automatic, the best unit will be chosen for all plots + values plotted to the axis (e.g. years for long periods, or km for long distances, or nanoseconds for short times). | ||
- `units_in_label=true`: controls, whether plots are shown in the label_prefix of the axis labels, or in the tick labels | ||
|
||
# Examples | ||
|
||
```julia | ||
using DynamicQuantities, CairoMakie | ||
|
||
# DQConversion will get chosen automatically, | ||
scatter(1:4, [1u"ns", 2u"ns", 3u"ns", 4u"ns"]) | ||
``` | ||
|
||
Fix unit to always use Meter & display unit in the xlabel: | ||
|
||
```julia | ||
# Temporary until this is upstreamed to Makie.jl | ||
const DQConversion = Base.get_extension(DynamicQuantities, :DynamicQuantitiesMakieExt).DQConversion | ||
|
||
dqc = DQConversion(us"m"; units_in_label=false) | ||
|
||
scatter(1:4, [0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]; axis=(dim2_conversion=dqc, xlabel="x (km)")) | ||
``` | ||
""" | ||
struct DQConversion <: M.AbstractDimConversion | ||
quantity::M.Observable{UnionAbstractQuantity{T1, SymbolicDimensions{T2}} where {T1 <: Real, T2 <: Real}} | ||
automatic_units::Bool | ||
units_in_label::M.Observable{Bool} | ||
end | ||
|
||
function DQConversion(quantity=M.automatic; units_in_label=true) | ||
return DQConversion(quantity, quantity isa M.Automatic, units_in_label) | ||
end | ||
|
||
M.needs_tick_update_observable(conversion::DQConversion) = conversion.quantity | ||
|
||
function M.get_ticks(conversion::DQConversion, ticks, scale, formatter, vmin, vmax) | ||
quantity = conversion.quantity[] | ||
quantity isa M.Automatic && return [], [] | ||
unit_str = unit_string(quantity) | ||
tick_vals, labels = M.get_ticks(ticks, scale, formatter, vmin, vmax) | ||
if conversion.units_in_label[] | ||
labels = labels .* unit_str | ||
end | ||
return tick_vals, labels | ||
end | ||
|
||
function M.convert_dim_observable(conversion::DQConversion, value_obs::M.Observable, deregister) | ||
# TODO: replace with update_extrema | ||
MilesCranmer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if conversion.automatic_units | ||
conversion.quantity[] = oneunit(value_obs[][1]) | ||
end | ||
result = map(conversion.quantity, value_obs; ignore_equal_values=true) do unit, values | ||
if !isempty(values) | ||
# try if conversion works, to through error if not! | ||
# Is there a function for this to check in DynamicQuantities? | ||
unit_convert(unit, values[1]) | ||
end | ||
return M.convert_dim_value(conversion, values) | ||
end | ||
append!(deregister, result.inputs) | ||
return result | ||
end | ||
|
||
function M.convert_dim_value(conversion::DQConversion, values) | ||
return unit_convert(conversion.quantity[], values) | ||
end | ||
|
||
@testitem "1 arg expansion" begin | ||
using DynamicQuantities, Makie, Dates | ||
|
||
f, ax, pl = scatter(u"m" .* (1:10)) | ||
@test pl isa Scatter{Tuple{Vector{Point2{Float64}}}} | ||
end | ||
|
||
@testitem "recipe" begin | ||
using DynamicQuantities, Makie, Dates | ||
const DQConversion = Base.get_extension(DynamicQuantities, :DynamicQuantitiesMakieExt).DQConversion | ||
|
||
@recipe(DQPlot, x) do scene | ||
return Attributes() | ||
end | ||
|
||
function Makie.plot!(plot::DQPlot) | ||
return scatter!(plot, plot.x, map(x -> x .* u"s", plot.x)) | ||
end | ||
|
||
f, ax, pl = dqplot(1:5) | ||
|
||
pl_conversion = Makie.get_conversions(pl) | ||
ax_conversion = Makie.get_conversions(ax) | ||
|
||
@test pl_conversion[2] isa DQConversion | ||
@test ax_conversion[2] isa DQConversion | ||
@test pl.plots[1][1][] == Point{2,Float32}.(1:5, 1:5) | ||
end | ||
|
||
@testitem "unit switching" begin | ||
using DynamicQuantities, Makie | ||
f, ax, pl = scatter((1:10)u"m") | ||
@test_throws DynamicQuantities.DimensionError scatter!(ax, (1:10)u"kg") | ||
@test_throws MethodError scatter!(ax, (1:10)) | ||
end | ||
|
||
@testitem "observables cleanup" begin | ||
using DynamicQuantities, Makie | ||
|
||
function test_cleanup(arg) | ||
obs = Observable(arg) | ||
f, ax, pl = scatter(obs) | ||
@test length(obs.listeners) == 1 | ||
delete!(ax, pl) | ||
@test length(obs.listeners) == 0 | ||
end | ||
|
||
test_cleanup([0.01u"km", 0.02u"km", 0.03u"km", 0.04u"km"]) | ||
end | ||
|
||
# TODO: Move upstream to Makie.jl | ||
@testitem "reftest" begin | ||
using DynamicQuantities, Makie | ||
const DQConversion = Base.get_extension(DynamicQuantities, :DynamicQuantitiesMakieExt).DQConversion | ||
|
||
fig = Figure() | ||
|
||
ax1 = Axis(fig[1, 1]; dim2_conversion=DQConversion(us"J/s")) | ||
ax2 = Axis(fig[1, 2]; dim2_conversion=DQConversion(us"mm/m^2")) | ||
ax3 = Axis(fig[2, 1]; dim1_conversion=DQConversion(us"W/m^2"), dim2_conversion=DQConversion(us"μm")) | ||
ax4 = Axis(fig[2, 2]; dim1_conversion=DQConversion(us"W/m^2")) | ||
|
||
scatter!(ax1, (1:10) .* u"J/s") | ||
scatter!(ax2, (1:10) .* u"K", exp.(1:10) .* u"mm/m^2") | ||
scatter!(ax3, 10 .^ (1:6) .* u"W/m^2", (1:6) .* 1000 .* u"nm") | ||
scatter!(ax4, (0:10) .* u"W/m^2", (0:10) .* u"g") | ||
scatter!(ax4, (0:10) .* u"kW/m^2", (0:10) .* u"kg") | ||
end | ||
|
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit simpler. Can you also use
;
rather thannothing
elsewhere tooThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, no dice just yet:
I noticed you mention this in your discussion with Abhro in #175, and I've wanted to try it out ever since. Is there something else that I could try? Found some relevant discussion here: JuliaDocs/Documenter.jl#1509