905 lines
27 KiB
Plaintext
905 lines
27 KiB
Plaintext
# Calculus plots with Makie
|
|
|
|
|
|
## XXX This needs a total rewrite for the new Makie
|
|
|
|
```julia; echo=false; results="hidden"
|
|
using CalculusWithJulia
|
|
using CalculusWithJulia.WeaveSupport
|
|
using AbstractPlotting
|
|
Base.showable(m::MIME"image/png", p::AbstractPlotting.Scene) = true # instruct weave to make graphs
|
|
nothing
|
|
```
|
|
|
|
The [Makie.jl webpage](https://github.com/JuliaPlots/Makie.jl) says
|
|
|
|
> From the Japanese word Maki-e, which is a technique to sprinkle lacquer with gold and silver powder. Data is basically the gold and silver of our age, so let's spread it out beautifully on the screen!
|
|
|
|
`Makie` itself is a metapackage for a rich ecosystem. We show how to
|
|
use the interface provided by `AbstractPlotting` and the `GLMakie`
|
|
backend to produce the familiar graphics of calculus. We do not
|
|
discuss the `MakieLayout` package which provides a means to layout
|
|
multiple graphics and add widgets, such as sliders and buttons, to a
|
|
layout. We do not discuss `MakieRecipes`. For `Plots`, there are
|
|
"recipes" that make some of the plots more straightforward. We do not
|
|
discuss the
|
|
[`AlgebraOfGraphics`](https://github.com/JuliaPlots/AlgebraOfGraphics.jl)
|
|
which presents an interface for the familiar graphics of statistics.
|
|
|
|
|
|
## Scenes
|
|
|
|
Makie draws graphics onto a canvas termed a "scene" in the Makie documentation. There are `GLMakie`, `WGLMakie`, and `CairoMakie` backends for different types of canvases. In the following, we have used `GLMakie`. `WGLMakie` is useful for incorporating `Makie` plots into web-based technologies.
|
|
|
|
We begin by loading our two packages:
|
|
|
|
```julia
|
|
using AbstractPlotting
|
|
using GLMakie
|
|
#using WGLMakie; WGLMakie.activate!()
|
|
#AbstractPlotting.set_theme!(scale_figure=false, resolution = (480, 400))
|
|
```
|
|
|
|
|
|
|
|
The `Makie` developers have workarounds for the delayed time to first plot, but without utilizing these the time to load the package is lengthy.
|
|
|
|
|
|
|
|
A scene is produced with `Scene()` or through a plotting primitive:
|
|
|
|
```julia
|
|
scene = Scene()
|
|
```
|
|
|
|
We see next how to move beyond the blank canvas.
|
|
|
|
## Points (`scatter`)
|
|
|
|
The task of plotting the points, say $(1,2)$, $(2,3)$, $(3,2)$ can be done different ways. Most plotting packages, and `Makie` is no exception, allow the following: form vectors of the $x$ and $y$ values then plot those with `scatter`:
|
|
|
|
```julia
|
|
xs = [1,2,3]
|
|
ys = [2,3,2]
|
|
scatter(xs, ys)
|
|
```
|
|
|
|
The `scatter` function creates and returns a `Scene` object, which when displayed shows the plot.
|
|
|
|
The more generic `plot` function can also be used for this task.
|
|
|
|
|
|
### `Point2`, `Point3`
|
|
|
|
When learning about points on the Cartesian plane, a "`t`"-chart is often produced:
|
|
|
|
```
|
|
x | y
|
|
-----
|
|
1 | 2
|
|
2 | 3
|
|
3 | 2
|
|
```
|
|
|
|
The `scatter` usage above used the columns. The rows are associated with the points, and these too can be used to produce the same graphic.
|
|
Rather than make vectors of $x$ and $y$ (and optionally $z$) coordinates, it is more idiomatic to create a vector of "points." `Makie` utilizes a `Point` type to store a 2 or 3 dimensional point. The `Point2` and `Point3` constructors will be utilized.
|
|
|
|
`Makie` uses a GPU, when present, to accelerate the graphic rendering. GPUs employ 32-bit numbers. Julia uses an `f0` to indicate 32-bit floating points. Hence the alternate types `Point2f0` to store 2D points as 32-bit numbers and `Points3f0` to store 3D points as 32-bit numbers are seen in the documentation for Makie.
|
|
|
|
|
|
We can plot vector of points in as direct manner as vectors of their coordinates:
|
|
|
|
```julia
|
|
pts = [Point2(1,2), Point2(2,3), Point2(3,2)]
|
|
scatter(pts)
|
|
```
|
|
|
|
A typical usage is to generate points from some vector-valued
|
|
function. Say we have a parameterized function `r` taking $R$ into
|
|
$R^2$ defined by:
|
|
|
|
```julia
|
|
r(t) = [sin(t), cos(t)]
|
|
```
|
|
|
|
|
|
Then broadcasting values gives a vector of vectors, each identified with a point:
|
|
|
|
```julia
|
|
ts = [1,2,3]
|
|
r.(ts)
|
|
```
|
|
|
|
We can broadcast `Point2` over this to create a vector of `Point` objects:
|
|
|
|
```julia
|
|
pts = Point2.(r.(ts))
|
|
```
|
|
|
|
These then can be plotted directly:
|
|
|
|
```julia
|
|
scatter(pts)
|
|
```
|
|
|
|
|
|
The ploting of points in three dimesions is essentially the same, save the use of `Point3` instead of `Point2`.
|
|
|
|
```julia
|
|
r(t) = [sin(t), cos(t), t]
|
|
ts = range(0, 4pi, length=100)
|
|
pts = Point3.(r.(ts))
|
|
scatter(pts)
|
|
```
|
|
|
|
|
|
----
|
|
|
|
To plot points generated in terms of vectors of coordinates, the
|
|
component vectors must be created. The "`t`"-table shows how, simply
|
|
loop over each column and add the corresponding $x$ or $y$ (or $z$)
|
|
value. This utility function does exactly that, returning the vectors
|
|
in a tuple.
|
|
|
|
```julia
|
|
unzip(vs) = Tuple([vs[j][i] for j in eachindex(vs)] for i in eachindex(vs[1]))
|
|
```
|
|
|
|
(The functionality is essentially a reverse of the `zip` function, hence the name.)
|
|
|
|
We might have then:
|
|
|
|
```julia
|
|
scatter(unzip(r.(ts))...)
|
|
```
|
|
|
|
where splatting is used to specify the `xs`, `ys`, and `zs` to `scatter`.
|
|
|
|
(Compare to `scatter(Point3.(r.(ts)))` or `scatter(Point3∘r).(ts))`.)
|
|
|
|
### Attributes
|
|
|
|
A point is drawn with a "marker" with a certain size and color. These attributes can be adjusted, as in the following:
|
|
|
|
```julia
|
|
scatter(xs, ys, marker=[:x,:cross, :circle], markersize=25, color=:blue)
|
|
```
|
|
|
|
Marker attributes include
|
|
|
|
* `marker` a symbol, shape. A single value will be repeated. A vector of values of a matching size will specify a marker for each point.
|
|
* `marker_offset` offset coordinates
|
|
* `markersize` size (radius pixels) of marker
|
|
|
|
|
|
### Text (`text`)
|
|
|
|
Text can be placed at a point, as a marker is. To place text the desired text and a position need to be specified.
|
|
|
|
For example:
|
|
|
|
```julia
|
|
pts = Point2.(1:5, 1:5)
|
|
scene = scatter(pts)
|
|
[text!(scene, "text", position=pt, textsize=1/i, rotation=2pi/i) for (i,pt) in enumerate(pts)]
|
|
scene
|
|
```
|
|
|
|
The graphic shows that `position` positions the text, `textsize` adjusts the displayed size, and `rotation` adjusts the orientation.
|
|
|
|
Attributes for `text` include:
|
|
|
|
* `position` to indicate the position. Either a `Point` object, as above, or a tuple
|
|
* `align` Specify the text alignment through `(:pos, :pos)`, where `:pos` can be `:left`, `:center`, or `:right`.
|
|
* `rotation` to indicate how the text is to be rotated
|
|
* `textsize` the font point size for the text
|
|
* `font` to indicate the desired font
|
|
|
|
|
|
## Curves
|
|
|
|
### Plots of univariate functions
|
|
|
|
The basic plot of univariate calculus is the graph of a function $f$ over an interval $[a,b]$. This is implemented using a familiar strategy: produce a series of representative values between $a$ and $b$; produce the corresponding $f(x)$ values; plot these as points and connect the points with straight lines. The `lines` function of `AbstractPlotting` will do the last step.
|
|
|
|
By taking a sufficient number of points within $[a,b]$ the connect-the-dot figure will appear curved, when the function is.
|
|
|
|
To create regular values between `a` and `b` either the `range` function, the related `LinRange` function, or the range operator (`a:h:b`) are employed.
|
|
|
|
|
|
For example:
|
|
|
|
```julia
|
|
f(x) = sin(x)
|
|
a, b = 0, 2pi
|
|
xs = range(a, b, length=250)
|
|
lines(xs, f.(xs))
|
|
```
|
|
|
|
Or
|
|
|
|
```julia
|
|
f(x) = cos(x)
|
|
a, b = -pi, pi
|
|
xs = a:pi/100:b
|
|
lines(xs, f.(xs))
|
|
```
|
|
|
|
As with `scatter`, `lines` returns a `Scene` object that produces a graphic when displayed.
|
|
|
|
As with `scatter`, `lines` can can also be drawn using a vector of points:
|
|
|
|
```juila
|
|
lines([Point2(x, fx) for (x,fx) in zip(xs, f.(xs))])
|
|
```
|
|
|
|
(Though the advantage isn't clear here, this will be useful when the points are more naturally generated.)
|
|
|
|
|
|
When a `y` value is `NaN` or infinite, the connecting lines are not drawn:
|
|
|
|
|
|
```
|
|
xs = 1:5
|
|
ys = [1,2,NaN, 4, 5]
|
|
lines(xs, ys)
|
|
```
|
|
|
|
As with other plotting packages, this is useful to represent discontinuous functions, such as what occurs at a vertical asymptote.
|
|
|
|
#### Adding to a scene (`lines!`, `scatter!`, ...)
|
|
|
|
To *add* or *modify* a scene can be done using a mutating version of a plotting primitive, such as `lines!` or `scatter!`. The names follow `Julia`'s convention of using an `!` to indicate that a function modifies an argument, in this case the scene.
|
|
|
|
Here is one way to show two plots at once:
|
|
|
|
```julia
|
|
xs = range(0, 2pi, length=100)
|
|
scene = lines(xs, sin.(xs))
|
|
lines!(scene, xs, cos.(xs))
|
|
```
|
|
|
|
We will see soon how to modify the line attributes so that the curves can be distinguished.
|
|
|
|
The following shows the construction details in the graphic, and that the initial scene argument is implicitly assumed:
|
|
|
|
```julia
|
|
xs = range(0, 2pi, length=10)
|
|
lines(xs, sin.(xs))
|
|
scatter!(xs, sin.(xs), markersize=10)
|
|
```
|
|
|
|
----
|
|
|
|
The current scene will have data limits that can be of interest. The following indicates how they can be manipulated to get the limits of the displayed `x` values.
|
|
|
|
```julia
|
|
xs = range(0, 2pi, length=200)
|
|
scene = plot(xs, sin.(xs))
|
|
rect = scene.data_limits[] # get limits for g from f
|
|
a, b = rect.origin[1], rect.origin[1] + rect.widths[1]
|
|
```
|
|
|
|
In the output it can be discerned that the values are 32-bit floating point numbers *and* yield a slightly larger interval than specified in `xs`.
|
|
|
|
|
|
As an example, this shows how to add the tangent line to a graph. The slope of the tangent line being computed by `ForwardDiff.derivative`.
|
|
|
|
```julia
|
|
using ForwardDiff
|
|
f(x) = x^x
|
|
a, b= 0, 2
|
|
c = 0.5
|
|
xs = range(a, b, length=200)
|
|
|
|
tl(x) = f(c) + ForwardDiff.derivative(f, c) * (x-c)
|
|
|
|
scene = lines(xs, f.(xs))
|
|
lines!(scene, xs, tl.(xs), color=:blue)
|
|
```
|
|
|
|
|
|
#### Attributes
|
|
|
|
In the last example, we added the argument `color=:blue` to the `lines!` call. This set an attribute for the line being drawn. Lines have other attributes that allow different ones to be distinguished, as above where colors indicate the different graphs.
|
|
|
|
Other attributes can be seen from the help page for `lines`, and include:
|
|
|
|
* `color` set with a symbol, as above, or a string
|
|
* `linestyle` available styles are set by a symbol, one of `:dash`, `:dot`, `:dashdot`, or `:dashdotdot`.
|
|
* `linewidth` width of line
|
|
* `transparency` the `alpha` value, a number between $0$ and $1$, smaller numbers for more transparent.
|
|
|
|
A legend can also be used to help identify different curves on the same graphic, though this is not illustrated. There are examples in the Makie gallery.
|
|
|
|
#### Scene attributes
|
|
|
|
Attributes of the scene include any titles and labels, the limits that define the coordinates being displayed, the presentation of tick marks, etc.
|
|
|
|
|
|
The `title` function can be used to add a title to a scene. The calling syntax is `title(scene, text)`.
|
|
|
|
To set the labels of the graph, there are "shorthand" functions `xlabel!`, `ylabel!`, and `zlabel!`. The calling pattern would follow `xlabel!(scene, "x-axis")`.
|
|
|
|
|
|
|
|
The plotting ticks and their labels are returned by the unexported functions `tickranges` and `ticklabels`. The unexported `xtickrange`, `ytickrange`, and `ztickrange`; and `xticklabels`, `yticklabels`, and `zticklabels` return these for the indicated axes.
|
|
|
|
These can be dynamically adjusted using `xticks!`, `yticks!`, or `zticks!`.
|
|
|
|
```julia
|
|
pts = [Point2(1,2), Point2(2,3), Point2(3,2)]
|
|
scene = scatter(pts)
|
|
title(scene, "3 points")
|
|
ylabel!(scene, "y values")
|
|
xticks!(scene, xtickrange=[1,2,3], xticklabels=["a", "b", "c"])
|
|
```
|
|
|
|
|
|
|
|
To set the limits of the graph there are shorthand functions `xlims!`, `ylims!`, and `zlims!`. This might prove useful if vertical asymptotes are encountered, as in this example:
|
|
|
|
```julia
|
|
f(x) = 1/x
|
|
a,b = -1, 1
|
|
xs = range(-1, 1, length=200)
|
|
scene = lines(xs, f.(xs))
|
|
ylims!(scene, (-10, 10))
|
|
center!(scene)
|
|
```
|
|
|
|
|
|
|
|
### Plots of parametric functions
|
|
|
|
A space curve is a plot of a function $f:R^2 \rightarrow R$ or $f:R^3 \rightarrow R$.
|
|
|
|
To construct a curve from a set of points, we have a similar pattern in both $2$ and $3$ dimensions:
|
|
|
|
```julia
|
|
r(t) = [sin(2t), cos(3t)]
|
|
ts = range(0, 2pi, length=200)
|
|
pts = Point2.(r.(ts)) # or (Point2∘r).(ts)
|
|
lines(pts)
|
|
```
|
|
|
|
Or
|
|
|
|
```julia
|
|
r(t) = [sin(2t), cos(3t), t]
|
|
ts = range(0, 2pi, length=200)
|
|
pts = Point3.(r.(ts))
|
|
lines(pts)
|
|
```
|
|
|
|
|
|
Alternatively, vectors of the $x$, $y$, and $z$ components can be produced and then plotted using the pattern `lines(xs, ys)` or `lines(xs, ys, zs)`. For example, using `unzip`, as above, we might have done the prior example with:
|
|
|
|
```julia
|
|
xs, ys, zs = unzip(r.(ts))
|
|
lines(xs, ys, zs)
|
|
```
|
|
|
|
|
|
#### Tangent vectors (`arrows`)
|
|
|
|
A tangent vector along a curve can be drawn quite easily using the `arrows` function. There are different interfaces for `arrows`, but we show the one which uses a vector of positions and a vector of "vectors". For the latter, we utilize the `derivative` function from `ForwardDiff`:
|
|
|
|
```julia
|
|
using ForwardDiff
|
|
r(t) = [sin(t), cos(t)] # vector, not tuple
|
|
ts = range(0, 4pi, length=200)
|
|
scene = Scene()
|
|
lines!(scene, Point2.(r.(ts)))
|
|
|
|
nts = 0:pi/4:2pi
|
|
us = r.(nts)
|
|
dus = ForwardDiff.derivative.(r, nts)
|
|
|
|
arrows!(scene, Point2.(us), Point2.(dus))
|
|
```
|
|
|
|
|
|
In 3 dimensions the differences are minor:
|
|
|
|
```julia
|
|
r(t) = [sin(t), cos(t), t] # vector, not tuple
|
|
ts = range(0, 4pi, length=200)
|
|
scene = Scene()
|
|
lines!(scene, Point3.(r.(ts)))
|
|
|
|
nts = pi:pi/4:3pi
|
|
us = r.(nts)
|
|
dus = ForwardDiff.derivative.(r, nts)
|
|
|
|
arrows!(scene, Point3.(us), Point3.(dus))
|
|
```
|
|
|
|
|
|
##### Attributes
|
|
|
|
Attributes for `arrows` include
|
|
|
|
* `arrowsize` to adjust the size
|
|
* `lengthscale` to scale the size
|
|
* `arrowcolor` to set the color
|
|
* `arrowhead` to adjust the head
|
|
* `arrowtail` to adjust the tail
|
|
|
|
### Implicit equations (2D)
|
|
|
|
The graph of an equation is the collection of all $(x,y)$ values satisfying the equation. This is more general than the graph of a function, which can be viewed as the graph of the equation $y=f(x)$. An equation in $x$-$y$ can be graphed if the set of solutions to a related equation $f(x,y)=0$ can be identified, as one can move all terms to one side of an equation and define $f$ as the rule of the side with the terms.
|
|
|
|
The [MDBM](https://github.com/bachrathyd/MDBM.jl) (Multi-Dimensional Bisection Method) package can be used for the task of characterizing when $f(x,y)=0$. (Also `IntervalConstraintProgramming` can be used.) We first wrap its interface and then define a "`plot`" recipe (through method overloading, not through `MakieRecipes`).
|
|
|
|
```julia
|
|
using MDBM
|
|
```
|
|
|
|
```julia
|
|
function implicit_equation(f, axes...; iteration::Int=4, constraint=nothing)
|
|
|
|
axes = [axes...]
|
|
|
|
if constraint == nothing
|
|
prob = MDBM_Problem(f, axes)
|
|
else
|
|
prob = MDBM_Problem(f, axes, constraint=constraint)
|
|
end
|
|
|
|
solve!(prob, iteration)
|
|
|
|
prob
|
|
end
|
|
```
|
|
|
|
The `implicit_equation` function is just a simplified wrapper for the `MDBM_Problem` interface. It creates an object to be plotted in a manner akin to:
|
|
|
|
```julia
|
|
f(x,y) = x^3 + x^2 + x + 1 - x*y # solve x^3 + x^2 + x + 1 = x*y
|
|
ie = implicit_equation(f, -5:5, -10:10)
|
|
```
|
|
|
|
The function definition is straightforward. The limits for `x` and `y` are specified in the above using ranges. This specifies the initial grid of points for the apdaptive algorithm used by `MDBM` to identify solutions.
|
|
|
|
To visualize the output, we make a new method for `plot` and `plot!`. There is a distinction between 2 and 3 dimensions. Below in two dimensions curve(s) are drawn. In three dimensions, scaled cubes are used to indicate the surface.
|
|
|
|
|
|
```julia
|
|
AbstractPlotting.plot(m::MDBM_Problem; kwargs...) = plot!(Scene(), m; kwargs...)
|
|
AbstractPlotting.plot!(m::MDBM_Problem; kwargs...) = plot!(AbstractPlotting.current_scene(), m; kwargs...)
|
|
AbstractPlotting.plot!(scene::AbstractPlotting.Scene, m::MDBM_Problem; kwargs...) =
|
|
plot!(Val(_dim(m)), scene, m; kwargs...)
|
|
|
|
_dim(m::MDBM.MDBM_Problem{a,N,b,c,d,e,f,g,h}) where {a,N,b,c,d,e,f,g,h} = N
|
|
```
|
|
|
|
Dispatch is used for the two different dimesions, identified through `_dim`, defined above.
|
|
|
|
```julia
|
|
# 2D plot
|
|
function AbstractPlotting.plot!(::Val{2}, scene::AbstractPlotting.Scene,
|
|
m::MDBM_Problem; color=:black, kwargs...)
|
|
|
|
mdt=MDBM.connect(m)
|
|
for i in 1:length(mdt)
|
|
dt=mdt[i]
|
|
P1=getinterpolatedsolution(m.ncubes[dt[1]], m)
|
|
P2=getinterpolatedsolution(m.ncubes[dt[2]], m)
|
|
lines!(scene, [P1[1],P2[1]],[P1[2],P2[2]], color=color, kwargs...)
|
|
end
|
|
|
|
scene
|
|
end
|
|
```
|
|
|
|
```julia
|
|
# 3D plot
|
|
function AbstractPlotting.plot!(::Val{3}, scene::AbstractPlotting.Scene,
|
|
m::MDBM_Problem; color=:black, kwargs...)
|
|
|
|
positions = Point{3, Float32}[]
|
|
scales = Vec3[]
|
|
|
|
mdt=MDBM.connect(m)
|
|
for i in 1:length(mdt)
|
|
dt=mdt[i]
|
|
P1=getinterpolatedsolution(m.ncubes[dt[1]], m)
|
|
P2=getinterpolatedsolution(m.ncubes[dt[2]], m)
|
|
|
|
a, b = Vec3(P1), Vec3(P2)
|
|
push!(positions, Point3(P1))
|
|
push!(scales, b-a)
|
|
end
|
|
|
|
cube = Rect{3, Float32}(Vec3(-0.5, -0.5, -0.5), Vec3(1, 1, 1))
|
|
meshscatter!(scene, positions, marker=cube, scale = scales, color=color, transparency=true, kwargs...)
|
|
|
|
scene
|
|
end
|
|
```
|
|
|
|
|
|
We see that the equation `ie` has two pieces. (This is known as Newton's trident, as Newton was interested in this form of equation.)
|
|
|
|
```julia
|
|
plot(ie)
|
|
```
|
|
|
|
|
|
|
|
## Surfaces
|
|
|
|
Plots of surfaces in 3 dimensions are useful to help understand the behavior of multivariate functions.
|
|
|
|
#### Surfaces defined through $z=f(x,y)$
|
|
|
|
The "peaks" function generates the logo for MATLAB. Here we see how it can be plotted over the region $[-5,5]\times[-5,5]$.
|
|
|
|
```julia
|
|
peaks(x,y) = 3*(1-x)^2*exp(-x^2 - (y+1)^2) - 10(x/5-x^3-y^5)*exp(-x^2-y^2)- 1/3*exp(-(x+1)^2-y^2)
|
|
xs = ys = range(-5, 5, length=25)
|
|
surface(xs, ys, peaks)
|
|
```
|
|
|
|
The calling pattern `surface(xs, ys, f)` implies a rectangular grid over the $x$-$y$ plane defined by `xs` and `ys` with $z$ values given by $f(x,y)$.
|
|
|
|
|
|
Alternatively a "matrix" of $z$ values can be specified. For a function `f`, this is conveniently generated by the pattern `f.(xs, ys')`, the `'` being important to get a matrix of all $x$-$y$ pairs through `Julia`'s broadcasting syntax.
|
|
|
|
```julia
|
|
zs = peaks.(xs, ys')
|
|
surface(xs, ys, zs)
|
|
```
|
|
|
|
|
|
To see how this graph is constructed, the points $(x,y,f(x,y))$ are plotted over the grid and displayed.
|
|
|
|
Here we downsample to illutrate
|
|
|
|
```julia
|
|
xs = ys = range(-5, 5, length=5)
|
|
pts = [Point3(x, y, peaks(x,y)) for x in xs for y in ys]
|
|
scatter(pts, markersize=25)
|
|
```
|
|
|
|
|
|
These points are connected. The `wireframe` function illustrates just the frame
|
|
|
|
```julia
|
|
wireframe(xs, ys, peaks.(xs, ys'), linewidth=5)
|
|
```
|
|
|
|
The `surface` call triangulates the frame and fills in the shading:
|
|
|
|
```julia
|
|
surface!(xs, ys, peaks.(xs, ys'))
|
|
```
|
|
|
|
#### Implicitly defined surfaces, $F(x,y,z)=0$
|
|
|
|
The set of points $(x,y,z)$ satisfying $F(x,y,z) = 0$ will form a surface that can be visualized using the `MDBM` package. We illustrate showing two nested surfaces.
|
|
|
|
```julia
|
|
r₂(x,y,z) = x^2 + y^2 + z^2 - 5/4 # a sphere
|
|
r₄(x,y,z) = x^4 + y^4 + z^4 - 1
|
|
xs = ys = zs = -2:2
|
|
m2,m4 = implicit_equation(r₂, xs, ys, zs), implicit_equation(r₄, xs, ys, zs)
|
|
|
|
plot(m4, color=:yellow)
|
|
plot!(m2, color=:red)
|
|
```
|
|
|
|
|
|
|
|
#### Parametrically defined surfaces
|
|
|
|
A surface may be parametrically defined through a function $r(u,v) = (x(u,v), y(u,v), z(u,v))$. For example, the surface generated by $z=f(x,y)$ is of the form with $r(u,v) = (u,v,f(u,v))$.
|
|
|
|
The `surface` function and the `wireframe` function can be used to display such surfaces. In previous usages, the `x` and `y` values were vectors from which a 2-dimensional grid is formed. For parametric surfaces, a grid for the `x` and `y` values must be generated. This function will do so:
|
|
|
|
```julia
|
|
function parametric_grid(us, vs, r)
|
|
n,m = length(us), length(vs)
|
|
xs, ys, zs = zeros(n,m), zeros(n,m), zeros(n,m)
|
|
for (i, uᵢ) in enumerate(us)
|
|
for (j, vⱼ) in enumerate(vs)
|
|
x,y,z = r(uᵢ, vⱼ)
|
|
xs[i,j] = x
|
|
ys[i,j] = y
|
|
zs[i,j] = z
|
|
end
|
|
end
|
|
(xs, ys, zs)
|
|
end
|
|
```
|
|
|
|
With the data suitably massaged, we can directly plot either a `surface` or `wireframe` plot.
|
|
|
|
----
|
|
|
|
As an aside, The above can be done more campactly with nested list comprehensions:
|
|
|
|
```
|
|
xs, ys, zs = [[pt[i] for pt in r.(us, vs')] for i in 1:3]
|
|
```
|
|
|
|
Or using the `unzip` function directly after broadcasting:
|
|
|
|
```
|
|
xs, ys, zs = unzip(r.(us, vs'))
|
|
```
|
|
|
|
----
|
|
|
|
For example, a sphere can be parameterized by $r(u,v) = (\sin(u)\cos(v), \sin(u)\sin(v), \cos(u))$ and visualized through:
|
|
|
|
```julia
|
|
r(u,v) = [sin(u)*cos(v), sin(u)*sin(v), cos(u)]
|
|
us = range(0, pi, length=25)
|
|
vs = range(0, pi/2, length=25)
|
|
xs, ys, zs = parametric_grid(us, vs, r)
|
|
|
|
scene = Scene()
|
|
surface!(scene, xs, ys, zs)
|
|
wireframe!(scene, xs, ys, zs)
|
|
```
|
|
|
|
A surface of revolution for $g(u)$ revolved about the $z$ axis can be visualized through:
|
|
|
|
```julia
|
|
g(u) = u^2 * exp(-u)
|
|
r(u,v) = (g(u)*sin(v), g(u)*cos(v), u)
|
|
us = range(0, 3, length=10)
|
|
vs = range(0, 2pi, length=10)
|
|
xs, ys, zs = parametric_grid(us, vs, r)
|
|
|
|
scene = Scene()
|
|
surface!(scene, xs, ys, zs)
|
|
wireframe!(scene, xs, ys, zs)
|
|
```
|
|
|
|
|
|
A torus with big radius $2$ and inner radius $1/2$ can be visualized as follows
|
|
|
|
```julia
|
|
r1, r2 = 2, 1/2
|
|
r(u,v) = ((r1 + r2*cos(v))*cos(u), (r1 + r2*cos(v))*sin(u), r2*sin(v))
|
|
us = vs = range(0, 2pi, length=25)
|
|
xs, ys, zs = parametric_grid(us, vs, r)
|
|
|
|
scene = Scene()
|
|
surface!(scene, xs, ys, zs)
|
|
wireframe!(scene, xs, ys, zs)
|
|
```
|
|
|
|
|
|
A Möbius strip can be produced with:
|
|
|
|
```julia
|
|
ws = range(-1/4, 1/4, length=8)
|
|
thetas = range(0, 2pi, length=30)
|
|
r(w, θ) = ((1+w*cos(θ/2))*cos(θ), (1+w*cos(θ/2))*sin(θ), w*sin(θ/2))
|
|
xs, ys, zs = parametric_grid(ws, thetas, r)
|
|
|
|
scene = Scene()
|
|
surface!(scene, xs, ys, zs)
|
|
wireframe!(scene, xs, ys, zs)
|
|
```
|
|
|
|
## Contour plots (`contour`, `heatmap`)
|
|
|
|
For a function $z = f(x,y)$ an alternative to a surface plot, is a contour plot. That is, for different values of $c$ the level curves $f(x,y)=c$ are drawn.
|
|
|
|
For a function $f(x,y)$, the syntax for generating a contour plot follows that for `surface`.
|
|
|
|
For example, using the `peaks` function, previously defined, we have a contour plot over the region $[-5,5]\times[-5,5]$ is generated through:
|
|
|
|
```julia
|
|
xs = ys = range(-5, 5, length=100)
|
|
contour(xs, ys, peaks)
|
|
```
|
|
|
|
The default of $5$ levels can be adjusted using the `levels` keyword:
|
|
|
|
```julia
|
|
contour(xs, ys, peaks.(xs, ys'), levels = 20)
|
|
```
|
|
|
|
(As a reminder, the above also shows how to generate values "`zs`" to pass to `contour` instead of a function.)
|
|
|
|
The contour graph makes identification of peaks and valleys easy as the limits of patterns of nested contour lines.
|
|
|
|
|
|
An alternative visualzation using color to replace contour lines is a heatmap. The `heatmap` function produces these. The calling syntax is similar to `contour` and `surface`:
|
|
|
|
|
|
```julia
|
|
heatmap(xs, ys, peaks)
|
|
```
|
|
|
|
This graph shows peaks and valleys through "hotspots" on the graph.
|
|
|
|
|
|
|
|
The `MakieGallery` package includes an example of a surface plot with both a wireframe and 2D contour graph added. It is replicated here using the `peaks` function scaled by $5$.
|
|
|
|
The function and domain to plot are described by:
|
|
|
|
```julia
|
|
xs = ys = range(-5, 5, length=51)
|
|
zs = peaks.(xs, ys') / 5;
|
|
```
|
|
|
|
The `zs` were generated, as `wireframe` does not provide the interface for passing a function.
|
|
|
|
The `surface` and `wireframe` are produced as follows:
|
|
|
|
```julia
|
|
scene = surface(xs, ys, zs)
|
|
wireframe!(scene, xs, ys, zs, overdraw = true, transparency = true, color = (:black, 0.1))
|
|
```
|
|
|
|
To add the contour, a simple call via `contour!(scene, xs, ys, zs)` will place the contour at the $z=0$ level which will make it hard to read. Rather, placing at the "bottom" of the scene is desirable. To identify that the "scene limits" are queried and the argument `transformation = (:xy, zmin)` is passed to `contour!`:
|
|
|
|
```julia
|
|
xmin, ymin, zmin = minimum(scene_limits(scene))
|
|
contour!(scene, xs, ys, zs, levels = 15, linewidth = 2, transformation = (:xy, zmin))
|
|
center!(scene)
|
|
```
|
|
|
|
The `transformation` plot attribute sets the "plane" (one of `:xy`, `:yz`, or `:xz`) at a location, in this example `zmin`.
|
|
|
|
|
|
|
|
|
|
### Three dimensional contour plots
|
|
|
|
The `contour` function can also plot $3$-dimensional contour plots. Concentric spheres, contours of $x^2 + y^2 + z^2 = c$ for $c > 0$ are presented by the following:
|
|
|
|
```julia
|
|
f(x,y,z) = x^2 + y^2 + z^2
|
|
xs = ys = zs = range(-3, 3, length=100)
|
|
contour(xs, ys, zs, f)
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Vector fields. Visualizations of $f:R^2 \rightarrow R^2$
|
|
|
|
The vector field $f(x,y) = (y, -x)$ can be visualized as a set of vectors, $f(x,y)$, positioned at a grid. These can be produced with the `arrows` function. Below we pass a vector of points for the anchors and a vector of points representing the vectors.
|
|
|
|
We can generate these on a regular grid through:
|
|
|
|
```julia
|
|
f(x, y) = [y, -x]
|
|
xs = ys = -5:5
|
|
pts = vec(Point2.(xs, ys'))
|
|
dus = vec(Point2.(f.(xs, ys')))
|
|
```
|
|
|
|
Broadcasting over `(xs, ys')` ensures each pair of possible values is encountered. The `vec` call reshapes an array into a vector.
|
|
|
|
Calling `arrows` on the prepared data produces the graphic:
|
|
|
|
```julia
|
|
arrows(pts, dus)
|
|
```
|
|
|
|
The grid seems rotated at first glance. This is due to the length of the vectors as the $(x,y)$ values get farther from the origin. Plotting the *normalized* values (each will have length $1$) can be done easily using `norm` (which requires `LinearAlgebra` to be loaded):
|
|
|
|
```julia
|
|
using LinearAlgebra
|
|
dvs = dus ./ norm.(dus)
|
|
arrows(pts, dvs)
|
|
```
|
|
|
|
The rotational pattern becomes quite clear now.
|
|
|
|
The `streamplot` function also illustrates this phenomenon. This implements an "algorithm [that] puts an arrow somewhere and extends the streamline in both directions from there. Then, it chooses a new position (from the remaining ones), repeating the the exercise until the streamline gets blocked, from which on a new starting point, the process repeats."
|
|
|
|
The `streamplot` function expects a `point` not a pair of values, so we adjust `f` slightly and call the function using the pattern `streamplot(f, xs, ys)`:
|
|
|
|
```julia
|
|
g(x,y) = Point2(f(x,y))
|
|
streamplot(g, xs, ys)
|
|
```
|
|
|
|
(The viewing range could also be adjusted with the `-5..5` notation from the `IntervalSets` package which is brought in when `AbstractPlotting` is loaded.)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Interacting with a scene
|
|
|
|
[Interaction](http://makie.juliaplots.org/stable/interaction.html) with a scene is very much integrated into `Makie`, as the design has a "sophisticated referencing system" which allows sharing of attributes. Adjusting one attribute, can then propogate to others.
|
|
|
|
|
|
In Makie, a `Node` is a structure that allows its value to be updated, similar to an array.
|
|
Nodes are `Observables`, which when changed can trigger an event. Nodes can rely on other nodes, so events can be cascaded.
|
|
|
|
A simple example is a means to dynamically adjust a label for a scene.
|
|
|
|
```
|
|
xs, = 1:5, rand(5)
|
|
scene = scatter(xs, ys)
|
|
```
|
|
|
|
We can create a "Node" through:
|
|
|
|
```
|
|
x = Node("x values")
|
|
```
|
|
|
|
The value of the node is retrieved by `x[]`, though the function call `to_value(x)` is recommened, as it will be defined even when `x` is not a node. This stored value could be used to set the $x$-label in our scene:
|
|
|
|
```
|
|
xlabel!(scene, x[])
|
|
```
|
|
|
|
We now set up an observer to update the label whenever the value of `x` is updated:
|
|
|
|
```
|
|
on(x) do val
|
|
xlabel!(scen, val)
|
|
end
|
|
```
|
|
|
|
Now setting the value of `x` will also update the label:
|
|
|
|
```
|
|
x[] = "The x axis"
|
|
```
|
|
|
|
|
|
A node can be more complicated. This shows how a node of $x$ value can be used to define dependent $y$ values. A scatter plot will update when the $x$ values are updated:
|
|
|
|
```
|
|
xs = Node(1:10)
|
|
ys = lift(a -> f.(a), xs)
|
|
```
|
|
|
|
The `lift` function lifts the values of `xs` to the values of `ys`.
|
|
|
|
These can be plotted directly:
|
|
|
|
```
|
|
scene = lines(xs, ys)
|
|
```
|
|
|
|
Changes to the `xs` values will be reflected in the `scene`.
|
|
|
|
```
|
|
xs[] = 2:9
|
|
```
|
|
|
|
We can incoporporate the two:
|
|
|
|
```
|
|
lab = lift(val -> "Showing from $(val.start) to $(val.stop)", xs)
|
|
on(lab) do val
|
|
xlabel!(scene, val)
|
|
udpate!(scene)
|
|
end
|
|
```
|
|
|
|
The `update!` call redraws the scene to adjust to increased or decreased range of $x$ values.
|
|
|
|
|
|
The mouse position can be recorded. An example in the gallery of examples shows how.
|
|
|
|
Here is a hint:
|
|
|
|
```
|
|
scene = lines(1:5, rand(5))
|
|
pos = lift(scene.events.mouseposition) do mpos
|
|
@show AbstractPlotting.to_world(scene, Point2f0(mpos))
|
|
end
|
|
```
|
|
|
|
This will display the coordinates of the mouse in the terminal, as the mouse is moved around.
|