add back makie plotting
This commit is contained in:
@@ -1,904 +0,0 @@
|
||||
# Calculus plots with Makie
|
||||
|
||||
XXX https://www.juliapackages.com/p/implicit3dplotting
|
||||
## 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.
|
||||
1155
quarto/alternatives/makie_plotting.qmd
Normal file
1155
quarto/alternatives/makie_plotting.qmd
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user