Files
CalculusWithJuliaNotes.jl/quarto/alternatives/makie_plotting.qmd
2026-08-11 17:17:08 -04:00

1272 lines
40 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Calculus plots with Makie
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 the `CairoMakie` backend to produce the familiar graphics of calculus.
:::{.callout-note}
## Examples and tutorials
`Makie` is a sophisticated plotting package, and capable of an enormous range of plots (cf. [examples](https://makie.juliaplots.org/stable/examples/plotting_functions/).) `Makie` also has numerous [tutorials](https://makie.juliaplots.org/stable/tutorials/) to learn from. These are far more extensive than what is described herein, as this section focuses just on the graphics from calculus.
:::
## Figures
Makie draws graphics onto a canvas termed a "scene" in the Makie documentation. A scene is an implementation detail, the basic (non-mutating) plotting commands described below return a `FigureAxisPlot` object, a compound object that combines a figure, an axes, and a plot object. We also briefly show the details of constructing a separate figure and axis.
The `show` method for figures displays the resulting graphic.
For `Makie` there are the different backends for different types of canvases. In the following, we have used `CairoMakie`.
We begin by loading the main package and the `norm` function from the standard `LinearAlgebra` package:
```{julia}
using CairoMakie
import LinearAlgebra: norm
```
```{julia}
#| echo: false
set_theme!(Figure = (size = (width, height),))
nothing
```
The package load time as of recent version of `Makie` is quite reasonable for a complicated project. (The time to first plot is around five seconds on a typical machine.)
## Points (`scatter`)
The task of plotting the points, say $(1,2)$, $(2,3)$, $(3,2)$ can be done different ways.
* We can plot two vectors holding the `x` and `y` coordinates, e.g. `[1,2,3]` and `[2,3,2]`.
* Using a tuple to represent a point, we can plot a vector of tuples, e.g. `[(1,2), (2,3), (3,2)]`.
* More idiomatically, using a `Point2` object to represent a point, a vector of point objects can be plotted, e.g. `[Point2(1,2), Point2(2,3), Point2(3,2)]`.
The `Point2` function also accepts a vector or tuple for input to describe the point. There is also `Point3` for 3-d plotting. `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.
It is not so difficult to convert between these storage formats. For example starting with
```{julia}
xs = [1, 2, 3]
ys = [2, 3, 2]
```
We can convert to a vector of tuples using broadcasting or `zip`:
```{julia}
tuple.(xs, ys), collect(zip(xs, ys))
```
A vector of points can be generated similarly:
```{julia}
Point2.(xs, ys), Point2.(zip(xs, ys))
```
The `unzip` function from the `CalculusWithJulia` package can reverse this direction:
```{julia}
using CalculusWithJulia: unzip # just SplitApplyCombine.invert
unzip(Point2.(xs, ys))
```
We illustrate in @fig-makie-plot-of-points-in-2-and-3 where we generate a vector of points using these two functions:
```{julia}
r(t) = [cos(t), sin(t)]
h(t) = (cos(t), sin(t), t)
ts = range(0, 2pi, 25)
```
::: {#fig-makie-plot-of-points-in-2-and-3 layout-ncol=2}
```{julia}
scatter(Point2.(r.(ts)))
```
```{julia}
scatter(Point3.(h.(ts)))
```
A scatter plot in 2d and 3d generated by broadscasting `Point2` over a vector of 2 component vectors and `Point3` over a vector of 3 component tuples.
:::
### Attributes of a marker
A point is drawn with a "marker" with a certain size and color. These attributes can be adjusted, as in @fig-makie-scatter-with-marker-attributes.
::: {#fig-makie-scatter-with-marker-attributes}
```{julia}
scatter(xs, ys;
marker=[:x,:cross, :circle],
markersize=25,
color=:blue)
```
Different marker attributes
:::
Marker attributes include
* `marker` a symbol, shape
* `markersize` size (radius pixels) of marker
* `marker_offset` offset coordinates
* `color` to adjust color
A single value will be repeated. A vector of values of a matching size will specify the attribute on a per point basis.
## Curves
A visualized curve in calculus is comprised of line segments. The `lines` command of `Makie` will render a curve by connecting a series of points with straight-line segments. By taking a sufficient number of points the connect-the-dot figure can appear curved.
### 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.
To create regular values between `a` and `b` typically the `range` function or the range operator (`a:h:b`) are employed. The related `LinRange` function is also an option.
For example:
::: {#fig-makie-basic-f-a-b-plot}
```{julia}
f(x) = sin(x)
a, b = 0, 2pi
xs = range(a, b, length=250)
lines(xs, f.(xs))
```
Use of `lines` to generate a curve
:::
Makie has a recipe that allows the `y` position to be just the function---`lines(xs, f)` would have generated @fig-makie-basic-f-a-b-plot as well.
`Makie` also will read the interval notation of `IntervalSets` and select its own set of intermediate points, so this command would also render the same plot as that of @fig-makie-basic-f-a-b-plot: `lines(a..b, f)`.
As with `scatter`, `lines` can also be drawn using a vector of points. (Though the advantage isn't clear here, this will be useful when the points are generated in different manners.) In this case, this approach would involve a command like `lines(Point2.(xs, f.(xs)))`.
As with `scatter`, `lines` returns an object that produces a graphic when displayed.
When a `y` value is `NaN` or infinite, the connecting lines are not drawn:
::: {#fig-makie-nan-disrupts-plotting}
```{julia}
xs = 1:5
ys = [1,2,NaN, 4, 5]
lines(xs, ys)
```
An `NaN` value in the `y` vector has no connecting line segment
:::
As with other plotting packages, this is useful to represent discontinuous functions, such as what occurs at a vertical asymptote or a step function.
#### Adding to a figure (`lines!`, `scatter!`, ...)
To *add* or *modify* a figure 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 underlying figure.
Here is one way to show two plots at once:
::: {#fig-makie-example-adding-a-layer}
```{julia}
xs = range(0, 2pi, length=100)
lines(xs, sin) # use function, not values from sin.(xs)
lines!(xs, cos)
current_figure()
```
Using `lines!` to add a layer
:::
:::{.callout-note}
## Current figure
The `current_figure` call is needed to have the figure display, as the returned value of `lines!` is not a figure object. (Figure objects display when shown as the output of a cell.)
:::
We will see soon how to modify the line attributes so that the curves can be distinguished.
@fig-makie-example-adding-a-layer-scatter shows the construction details to produce the graphic.
::: {#fig-makie-example-adding-a-layer-scatter}
```{julia}
xs = range(0, 2pi, length=10)
lines(xs, sin)
scatter!(xs, sin;
markersize=10)
current_figure()
```
Using `scatter!` to show the points used when creating a dot-to-dot plot
:::
As an example, @fig-makie-example-adding-a-layer-tangent-line shows how to add the tangent line to a graph. The slope of the tangent line being computed by `ForwardDiff.derivative`.
::: {#fig-makie-example-adding-a-layer-tangent-line}
```{julia}
using ForwardDiff: derivative
f(x) = x^x
a, b= 0, 2
c = 0.5
xs = range(a, b, length=200)
tl(x) = f(c) + derivative(f, c) * (x-c)
lines(xs, f)
lines!(xs, tl, color=:blue)
current_figure()
```
Adding a tangent line to a curve at a point
:::
This example^[This example is modified from a [discourse](https://discourse.julialang.org/t/how-to-plot-step-functions-x-correctly-in-julia/84087/5) post by user `@rafael.guerra`.] shows how to plot a step function (`floor`) using `NaN`s to create line breaks. The marker colors set for `scatter!` use `:white` to match the background color.
::: {#fig-makie-example-adding-a-layer-step-function}
```{julia}
x = -5:5
δ = 5eps() # for rounding purposes; our interval is [i,i+1) ≈ [i, i+1-δ]
xx = Float64[]
for i ∈ x[1:end-1]
append!(xx, (i, i+1 - δ, NaN))
end
yy = floor.(xx)
lines(xx, yy)
scatter!(xx, yy, color=repeat([:black, :white, :white], length(xx)÷3))
current_figure()
```
Using layers to plot a step function
:::
### Text (`annotations`)
Text can be placed at a point, as a marker is. To place text, the desired text and a position need to be specified along with any adjustments to the default attributes.
For example @fig-makie-example-adding-a-layer-annotations show an annotation and the use of `fontsize` to adjust the displayed text size.
::: {#fig-makie-example-adding-a-layer-annotations}
```{julia}
xs = 1:5
pts = Point2.(xs, xs)
scatter(pts)
annotation!(pts;
text = "Point " .* string.(xs),
fontsize = 30 .- 5*xs)
current_figure()
```
Using `annotation!` to add text within a graphic
:::
Attributes for `text`, among many others, include:
* `align` Specify the text alignment through `(:pos, :pos)`, where `:pos` can be `:left`, `:center`, or `:right`.
* `fontsize` the font point size for the text
* `font` to indicate the desired font
Annotations with an arrow can be useful to highlight a feature of a graph. The code to produce @fig-makie-example-adding-a-layer-annotations-arrows is modified from the documentation of Makie; it utilizes some interval functions to draw an arrow with an arc.^[This example annotates the underlying `Axis` object, extracted using tuple destructuring. A more direct approach of creating a `Figure` object and then an `Axis` object will be illustrated later.]
::: {#fig-makie-example-adding-a-layer-annotations-arrows}
```{julia}
g(x) = cos(6x) * exp(x)
xs = 0:0.01:4
# this next line gets the Axis object from the FigureAxisPlot object
_, ax, _ = lines(xs, g;
axis = (; xgridvisible = false, ygridvisible = false))
annotation!(ax, 1, 20, 2.1, g(2.1), # annotate an Axis object
text = "A relative maximum",
path = Ann.Paths.Arc(0.3),
style = Ann.Styles.LineArrow(),
labelspace = :data
)
current_figure()
```
Use of `path` and `style` attributes of `annotation!` to draw an arrow
:::
#### Line attributes
In a previous example, we added the argument `color=:blue` to the `lines!` call. This was to 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
* `label` a label for the line to display in a legend
* `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.
#### Simple legends
A simple legend displaying labels given to each curve can be produced by `axislegend`. For example:
```{julia}
xs = 0..pi
lines(xs, x -> sin(x^2), label="sin(x^2)")
lines!(xs, x -> sin(x)^2, label = "sin(x)^2")
axislegend()
current_figure()
```
Later, we will see how to control the placement of a legend within a figure.
#### Titles, axis labels, axis ticks
The basic plots we have seen are of type `FigureAxisPlot`. The "axis" part controls attributes of the plot such as titles, labels, tick positions, etc. These values can be set in different manners. On construction we can pass values to a named argument `axis` using a named tuple.
For example:
::: {#fig-makie-title-axis-label-xlabel-ylabel}
```{julia}
xs = 0..2pi
lines(xs, sin;
axis=(title="Plot of sin(x)", xlabel="x", ylabel="sin(x)")
)
```
Passing `title`, `xlabel`, and `ylabel` values to the underlying axis
:::
To access the `axis` element of a plot **after** the plot is constructed, values can be assigned to the `axis` property of the `FigureAxisPlot` object. For example:
::: {#fig-makie-title-axis-label-xlabel-ylabel-after-construction}
```{julia}
xs = 0..2pi
p = lines(xs, sin;
axis=(title="Plot of sin(x)", xlabel="x", ylabel="sin(x)")
)
p.axis.xticks = MultiplesTicks(5, pi, "π") # label 5 times using `pi`
current_figure()
```
One way to access the underlying axis after construction. (The more systematic way is to produce a `Figure` and construct an `Axis` object to modify.)
:::
The ticks are most easily set as a collection of values. Above, the `MultiplesTicks` function was used to label with multiples of $\pi$.
Later we will discuss how `Makie` allows for subsequent modification of several parts of the plot (not just the ticks) including the data.
#### Figure size, $x$ and $y$ limits
As just mentioned, the basic plots we have seen are of type `FigureAxisPlot`. The "figure" part can be used to adjust the background color or the size. As with attributes for the axis, these too can be passed to a simple constructor:
::: {#fig-makie-title-axis-figure-size}
```{julia}
lines(xs, sin;
axis=(title="Plot of sin(x)", xlabel="x", ylabel="sin(x)"),
figure=(;size=(300, 300))
)
```
Adjust `size` value for the enclosing `Figure` object
:::
The `;` in the tuple passed to `figure` is one way to create a *named* tuple with a single element.
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 the code to produce @fig-makie-xlims-ylims.
::: {#fig-makie-xlims-ylims}
```{julia}
f(x) = 1/x
a,b = -1, 1
xs = range(-1, 1, length=200)
lines(xs, f)
ylims!(-10, 10)
current_figure()
```
Adjusting the range of possible `y` values with `ylims!`
:::
Adjusting the `y` limits still leaves an artifact due to the vertical asymptote at $0$ having different values from the left and the right.
### 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:
::: {#fig-makie-parametric-plot-examples layout-ncol=2}
```{julia}
r(t) = [sin(2t), cos(3t)]
ts = range(0, 2pi, length=200)
pts = Point2.(r.(ts)) # or (Point2∘r).(ts)
lines(pts)
```
```{julia}
r(t) = [sin(2t), cos(3t), t]
ts = range(0, 2pi, length=200)
pts = Point3.(r.(ts))
lines(pts)
```
Two and three dimensional parametric plots
:::
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 `lines(unzip(r.(ts))...)`.
#### Aspect ratio
A simple plot of a parametrically defined circle will show an ellipse, as the aspect ratio of the $x$ and $y$ axis is not $1$. To enforce this, we can pass a value of `aspect=1` to the underlying "Axis" object. @fig-makie-aspect-ratio-1 provides an example.
::: {#fig-makie-aspect-ratio-1}
```{julia}
ts = range(0, 2pi, length=100)
lines(sin.(ts), cos.(ts);
axis=(; aspect = 1))
```
Passing `aspect=1` to the axis make the `x` and `y`-axis scales equal
:::
#### 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`. In 3 dimensions the differences are minor, as seen in the code to produce @fig-makie-parametric-plot-tangent-vectors.
::: {#fig-makie-parametric-plot-tangent-vectors layout-ncol=2}
```{julia}
r(t) = [sin(t), cos(t)] # vector, not tuple
ts = range(0, 4pi, length=200)
lines(Point2.(r.(ts)))
nts = 0:pi/4:2pi
us = r.(nts)
dus = derivative.(r, nts)
arrows2d!(Point2.(us), Point2.(dus))
current_figure()
```
and
```{julia}
r(t) = [sin(t), cos(t), t] # vector, not tuple
ts = range(0, 4pi, length=200)
lines(Point3.(r.(ts)))
nts = 0:pi/2:(4pi-pi/2)
us = r.(nts)
dus = derivative.(r, nts)
arrows3d!(Point3.(us), Point3.(dus))
current_figure()
```
Plot of tangent lines in both two and three dimenstions
:::
#### Arrow 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
## Surfaces
Plots of surfaces in $3$ dimensions are useful to help understand the behavior of multivariate functions. There are a few common visualizations.
#### Surfaces defined through $z=f(x,y)$
The "`peaks`" function defined below has a few prominent peaks:
```{julia}
function peaks(x, y)
p = 3*(1-x)^2*exp(-x^2 - (y+1)^2)
p -= 10(x/5-x^3-y^5)*exp(-x^2-y^2)
p -= 1/3*exp(-(x+1)^2-y^2)
p
end
```
@fig-makie-surface-peaks shows how `peaks` can be visualized over the region $[-5,5]\times[-5,5]$:
::: {#fig-makie-surface-peaks}
```{julia}
xs = ys = range(-5, 5, length=25)
surface(xs, ys, peaks)
```
Surface plot produced by `surface(xs, ys, f)`
:::
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.
::: {#fig-makie-surface-using-broadcasting}
```{julia}
zs = peaks.(xs, ys')
surface(xs, ys, zs);
```
Surface generated by `surface(xs, ys, zs)` where `zs` is a matrix of values
:::
##### Example: surface graph construction
To see how a surface graph is constructed, the points $(x,y,f(x,y))$ are plotted over the grid and displayed.
In this examplpe, we downsample to illustrate.
::: {#fig-makie-surface-plot-scatter}
```{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)
```
Points used in downsampled graphic
:::
The points in @fig-makie-surface-plot-scatter are then connected. The `wireframe` function illustrates just the frame in @fig-makie-surface-plot-wireframe.
::: {#fig-makie-surface-plot-wireframe}
```{julia}
wireframe(xs, ys, peaks.(xs, ys'); linewidth=5)
```
Wireframe used in downsampled graphic
:::
The `surface` call triangulates the frame and fills in the shading.
::: {#fig-makie-surface-plot-surface}
```{julia}
surface!(xs, ys, peaks.(xs, ys'))
current_figure()
```
The `surface` shading used in the downsampled graphic
:::
#### 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 pairs(us)
for (j, vⱼ) in pairs(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 these commands to produce @fig-makie-parametric-sphere.
::: {#fig-makie-parametric-sphere}
```{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)
surface(xs, ys, zs)
wireframe!(xs, ys, zs)
current_figure()
```
Part of sphere plotted using a parametric description of the data
:::
A surface of revolution for $g(u)$ revolved about the $z$ axis can be visualized through the commands to produce @fig-makie-parametric-surface-revolution.
::: {#fig-makie-parametric-surface-revolution}
```{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)
surface(xs, ys, zs)
wireframe!(xs, ys, zs)
current_figure()
```
Surface of revolution formed by revolving `g` around the `z` axis.
:::
A torus with big radius $2$ and inner radius $1/2$ is visualized in @fig-makie-parametric-surface-torus.
::: {#fig-makie-parametric-surface-torus}
```{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)
surface(xs, ys, zs)
wireframe!(xs, ys, zs)
current_figure()
```
Surface of torus plotted as a parametrically defined surface
:::
A Möbius strip is produced in @fig-makie-parametric-surface-mobius-strip.
::: {#fig-makie-parametric-surface-mobius-strip}
```{julia}
r(w, θ) = ((1+w*cos(θ/2))*cos(θ), (1+w*cos(θ/2))*sin(θ), w*sin(θ/2))
ws = range(-1/4, 1/4, length=8)
thetas = range(0, 2pi, length=30)
xs, ys, zs = parametric_grid(ws, thetas, r)
surface(xs, ys, zs)
wireframe!(xs, ys, zs)
current_figure()
```
A Möbius strip can be parameterized and displayed
:::
## Contour plots (`contour`, `contourf`, `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 `contour(xs, ys, peaks)`. A figure is shown in @fig-makie-contour-peaks`.
::: {#fig-makie-contour-peaks}
```{julia}
xs = ys = range(-5, 5, length=100)
contour(xs, ys, peaks)
```
Contour plot of `peaks`
:::
The default of $5$ levels can be adjusted using the contour function's `levels` keyword. @fig-makie-contour-peaks-levels show the `peaks` function with `levels = 20`. The `levels` argument can also specify precisely what levels are to be drawn.
::: {#fig-makie-contour-peaks-levels}
```{julia}
contour(xs, ys, peaks; levels = 20)
```
Contour plot of `peaks` with the display of 20 levels specified
:::
The contour graph makes identification of peaks and valleys easy as the limits of patterns of nested contour lines.
A *filled* contour plot is produced by `contourf`, as seen in @fig-makie-contour-peaks-filled.
::: {#fig-makie-contour-peaks-filled}
```{julia}
contourf(xs, ys, peaks)
```
The `contourf` command produces filled contour plots
:::
A related, but alternative visualization, using color to represent magnitude is a heatmap, produced by the `heatmap` function. The calling syntax is similar to `contour` and `surface`. @fig-makie-heatmatp-peaks shows peaks and valleys through "hotspots" on the graph.
::: {#fig-makie-heatmatp-peaks}
```{julia}
heatmap(xs, ys, peaks)
```
Heatmap of `peaks` function
:::
##### Example
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` graphics are produced as follows. In the following we manually create the figure and axis object (using `Figure` and `Axis` as shown). We do this to set the viewing angle through the `elevation` argument to the axis object. We plot onto this axis in producing @fig-makie-peaks-surface-wireframe and then display the `Figure` object.
::: {#fig-makie-peaks-surface-wireframe}
```{julia}
fig = Figure()
ax3 = Axis3(fig[1,1]; # upper left of `fig`
elevation=pi/9, azimuth=pi/16)
surface!(ax3, xs, ys, zs)
wireframe!(ax3, xs, ys, zs;
overdraw = true, transparency = true,
color = (:black, 0.1))
fig # Figure object displays graphic
```
Surface and wireframe for the `peaks` function
:::
Next, we add a contour graph to @fig-makie-peaks-surface-wireframe to produce @fig-makie-peaks-surface-wireframe-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 figure is desirable.
To identify that the minimum value, is identified (and rounded) and the argument `transformation = (:xy, zmin)` is passed to `contour!`:
::: {#fig-makie-peaks-surface-wireframe-contour}
```{julia}
zmin, zmax = extrema(zs)
zmin, zmax = floor(zmin), ceil(zmax) # round down/up
contour!(ax3, xs, ys, zs;
levels = 15, linewidth = 2,
transformation = (:xy, zmin))
zlims!(ax3, zmin, zmax)
fig
```
Surface, wireframe, and contour for the `peaks` function
:::
The `transformation` plot attribute sets the "plane" (one of `:xy`, `:yz`, or `:xz`) at a location, in this example `zmin`.
The manual construction of a figure and an axis object will be further discussed later.
### 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 in @fig-makie-countour-three-d.
::: {#fig-makie-countour-three-d}
```{julia}
f(x,y,z) = x^2 + y^2 + z^2
xs = ys = zs = range(-3, 3, length=100)
contour(xs, ys, zs, f)
```
Three dimensional contour plot
:::
### Implicitly defined curves and surfaces
Suppose $f$ is a scalar-valued function. If `f` takes two variables for its input, then the equation $f(x,y) = 0$ implicitly defines $y$ as a function of $x$; $y$ can be visualized *locally* with a curve. If $f$ takes three variables for its input, then the equation $f(x,y,z)=0$ implicitly defines $z$ as a function of $x$ and $y$; $z$ can be visualized *locally* with a surface.
#### Implicitly defined curves
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 implicit function theorem ensures that under some conditions, *locally* near a point $(x, y)$, the value $y$ can be represented as a function of $x$. So, the graph of the equation $f(x,y)=0$ can be produced by stitching together these local function representations.
The contour graph can produce these graphs by setting the `levels` argument to `[0]`.
::: {#fig-makie-implicit-plot}
```{julia}
f(x,y) = x^3 + x^2 + x + 1 - x*y # solve x^3 + x^2 + x + 1 = x*y
xs = range(-5, 5, length=100)
ys = range(-10, 10, length=100)
contour(xs, ys, f; levels=[0])
```
Using `contour` to graph an implicitly defined function
:::
The `implicitPlots.jl` function uses the `Contour` package along with a `Plots` recipe to plot such graphs. Here we see how to use `Makie` in a similar manner:
```{julia}
#| eval: false
import Contour
function implicit_plot(xs, ys, f; kwargs...)
fig = Figure()
ax = Axis(fig[1,1])
implicit_plot!(ax, xs, ys, f; kwargs...)
fig
end
function implicit_plot!(ax, xs, ys, f; kwargs...)
z = [f(x, y) for x in xs, y in ys]
cs = Contour.contour(collect(xs), collect(ys), z, 0.0)
ls = Contour.lines(cs)
isempty(ls) && error("empty")
for l ∈ ls
us, vs = Contour.coordinates(l)
lines!(ax, us, vs; kwargs...)
end
end
```
#### Implicitly defined surfaces, $F(x,y,z)=0$
To plot the equation $F(x,y,z)=0$, for $F$ a scalar-valued function, again the implicit function theorem says that, under conditions, near any solution $(x,y,z)$, $z$ can be represented as a function of $x$ and $y$, so the graph will look like surfaces stitched together.
With `Makie`, many implicitly defined surfaces can be adequately represented using `contour` with the attribute `levels=[0]`. We will illustrate this technique.
::: {.callout-note}
## GLMakie
The `CairoMakie` backend does not handle these next few plots, so we use `GLMakie.contour` in the following after importing with:
```{julia}
import GLMakie
```
:::
To begin, we plot a sphere implicitly as a solution to $F(x,y,z) = x^2 + y^2 + z^2 - 1 = 0$ in @fig-makie-implicit-plot-3d.
::: {#fig-makie-implicit-plot-3d}
```{julia}
f(x) = norm(x)^2 - 1
ϕ(x,y,z) = (x,y,z)
xs = ys = zs = range(-3/2, 3/2, 100)
GLMakie.contour(xs, ys, zs, f∘ϕ; levels=[0], colormap=:RdBu)
```
Three dimensional implicitly defined surface plotted with `contour`
:::
@fig-makie-implicit-plot-3d-intersection visualizes an intersection of a sphere with another figure. To show the different surfaces, different colormaps are chosen.
::: {#fig-makie-implicit-plot-3d-intersection}
```{julia}
r₂(x) = sum(x.^2) - 2 # a sphere
r₄(x) = sum(x.^4) - 1
ϕ(x,y,z) = (x,y,z)
xs = ys = zs = range(-2, 2, 100)
GLMakie.contour(xs, ys, zs, r₂∘ϕ; levels = [0], colormap=:RdBu)
GLMakie.contour!(xs, ys, zs, r₄∘ϕ; levels = [0], colormap=:viridis)
current_figure()
```
Two implicitly defined surfaces in three dimensions disambiguated through different colormaps
:::
@fig-makie-implicit-genus-2-function presents an example from [Wikipedia](https://en.wikipedia.org/wiki/Implicit_surface) showing an implicit surface of genus $2$.
::: {#fig-makie-implicit-genus-2-function}
```{julia}
f(x,y,z) = 2y*(y^2 -3x^2)*(1-z^2) + (x^2 +y^2)^2 - (9z^2-1)*(1-z^2)
xs = ys = zs = range(-5/2, 5/2, 100)
GLMakie.contour(xs, ys, zs, f; levels=[0], colormap=:RdBu)
```
Implicit surface of a genus 2 function. This figure does not render well though, as the hole is not displayed.
:::
The `Implicit3DPlotting` package takes an approach like `ImplicitPlots` to represent these surfaces. It replaces the `Contour` package computation with a $3$-dimensional alternative provided through the `Meshing` and `GeometryBasics` packages. This package has a `plot_implicit_surface` function that does something similar as just illustrated, but handles the "hole" not shown in @fig-makie-implicit-genus-2-function.
The `plot_implicit_surface` takes a function of a single argument, so we wrap `f` within `splat` which takes that single argument and "splats" them so `f` can be used. The result appears in @fig-makie-implicit-genus-2-function-Implicit3DPlotting.
::: {#fig-makie-implicit-genus-2-function-Implicit3DPlotting}
```{julia}
using Implicit3DPlotting
plot_implicit_surface(splat(f); xlims=(-5/2, 5/2), ylims=(-5/2, 5/2))
```
Implicit surface of a genus 2 function. This surface, rendered with the `Implicit3DPlotting` package, shows the holes present in the surface.
:::
For one last example from Wikipedia, we have the Cassini oval which "can be defined as the point set for which the *product* of the distances to $n$ given points is constant."
::: {#fig-makie-implicit-cassini-oval}
```{julia}
function cassini(λ, ps = ((1,0,0), (-1, 0, 0))) # cassini returns a function
n = length(ps)
x -> prod(norm(x .- p) for p ∈ ps) - λ^n
end
xs = ys = zs = range(-3/2, 3/2, 100)
GLMakie.contour(xs, ys, zs, cassini(0.80) ∘ ϕ; levels=[0], colormap=:RdBu)
```
Cassini oval implicitly defined
:::
## Vector fields. Visualizations of $f:R^2 \rightarrow R^2$
The vector field $f(x,y) = \langle y, -x \rangle$ can be visualized as a set of vectors, $f(x,y)$, positioned at a grid. These arrows can be visualized with the `arrows` function. The `arrows` function is passed 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')));
first(pts), first(dus) # show an example
```
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:
::: {#fig-makie-arrows-vector-field}
```{julia}
arrows2d(pts, dus)
```
Vectorfield plot generated by `arrows2d`. Modification is needed.
:::
The grid seems rotated at first glance; but is also confusing. 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 is found in the standard `LinearAlgebra` library):
::: {#fig-makie-arrows-vector-field-modified}
```{julia}
dvs = dus ./ norm.(dus)
arrows2d(pts, dvs)
```
Vectorfield plot generated by `arrows2d` after modification
:::
The rotational pattern in @fig-makie-arrows-vector-field-modified is much clearer than from @fig-makie-arrows-vector-field.
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 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(g, xs, ys)`:
::: {#fig-makie-arrows-vector-field-streamplot}
```{julia}
f(x, y) = [y, -x]
g(xs) = Point2(f(xs...))
streamplot(g, -5..5, -5..5)
```
Graph produced by `streamplot`
:::
(We used interval notation to set the viewing range, a range could also be used.)
:::{.callout-note}
## Note
The calling pattern of `streamplot` is different than other functions, such as `surface`, in that the function comes first.
:::
## Layoutables
The `FigureAxisPlot` comprises an enclosing figure and one or more axes. These can be constructed directly through a pattern like the following:
::: {#fig-makie-figure-axis}
```{julia}
F = Figure() # can pass size=(w,h)
ax = Axis(F[1,1]) # can pass title, xlabel, ylabel, ...
ylims!(ax, (-5, 5)) # can limit viewing window size for an axis
xs = range(-8, 8, 100)
ys = xs .+ sin.(xs)
lines!(ax, xs, ys) # layer on the axis, not the figure
F # display figure
```
Basic pattern to construct a figure and an axis to layer on
:::
`Makie` makes it really easy to piece together figures from individual plots. To illustrate, we create a graphic consisting of a plot of a function, its derivative, and its second derivative. In our graphic, we also leave space for a label.
:::{.callout-note}
## Note
The Layout [Tutorial](https://makie.juliaplots.org/stable/tutorials/layout-tutorial/) has *much* more detail on this subject.
:::
For laying out our own composite graphic, we manage the figure and axes manually. The commands below create a figure, then assign axes to certain portions of the figure:
```{julia}
F = Figure()
#af = F[2,1:2] = Axis(F)
#afp = F[3,1:end] = Axis(F)
#afpp = F[4,:] = Axis(F)
af = Axis(F[2,1:2])
afp = Axis(F[3,1:end])
afpp = Axis(F[4,:])
```
The axes are named `af`, `afp` and `afpp`, as they will hold the respective graphs. The key here is the use of matrix notation to layout the graphic in a grid. The first one is row 2 and columns 1 through 2; the second row 3 and again all columns, the third is row 4 and all columns.
In this figure, we want the $x$-axis for each of the three graphics to be linked. This command ensures that:
```{julia}
linkxaxes!(af, afp, afpp);
```
By linking axes, if one is updated, say through `xlims!`, the others will be as well.
We now plot our functions. The key here is the mutating form of `lines!` takes an axis object to mutate as its first argument:
```{julia}
f(x) = 8x^4 - 8x^2 + 1
fp(x) = 32x^3 - 16x
fpp(x) = 96x^2 - 16
xs = -1..1
lines!(af, xs, f)
lines!(afp, xs, fp)
lines!(afp, xs, zero, color=:blue)
lines!(afpp, xs, fpp)
lines!(afpp, xs, zero, color=:blue);
```
We can give title information to each axis on construction or after construction, through commands like:
```{julia}
af.title = "f"
afp.title = "fp"
afpp.title = "fpp";
```
Finally, we add a label in the first row, but for illustration purposes, only use the first column.
```{julia}
Label(F[1,1], """
Plots of f and its first and second derivatives.
When the first derivative is zero, the function
f has relative extrema. When the second derivative
is zero, the function f has an inflection point.
""");
```
We display the figure in @fig-makie-layoutables
::: {#fig-makie-layoutables}
```{julia}
F
```
Figure containing multiple axes
:::
## Observables
::: {.callout-note}
## This needs updating
As over `v"0.24"` of Makie, there is an alternative to using `Observables`. These notes need updating to reflect that change.
:::
The basic components of a plot in `Makie` can be updated [interactively](https://makie.juliaplots.org/stable/documentation/nodes/index.html#observables_interaction). Historically `Makie` used the `Observables` package which allows complicated interactions to be modeled quite naturally. In the following we give a simple example, though newer versions of `Makie` rely on a different mechanism.
In Makie, an `Observable` is a structure that allows its value to be updated, similar to an array. When changed, observables can trigger an event. Observables can rely on other observables, so events can be cascaded.
This simple example shows how an observable `h` can be used to create a collection of points representing a secant line. The figure shows the value for `h=3/2`.
::: {#fig-makie-observable}
```{julia}
begin
f(x) = sqrt(x)
c = 1
xs = 0..3
h = Observable(3/2)
points = lift(h) do h
xs = [0,c,c+h,3]
tl = x -> f(c) + (f(c+h)-f(c))/h * (x-c)
[Point2(x, tl(x)) for x ∈ xs]
end
lines(xs, f)
lines!(points)
current_figure()
end
```
Illustration of using an `Observable`
:::
We can update the value of `h` using `setindex!` notation (square brackets). For example, to see that the secant line is a good approximation to the tangent line as $h \rightarrow 0$ we can set `h` to be `1/4` and replot in @fig-makie-observable-updated.
::: {#fig-makie-observable-updated}
```{julia}
h[] = 1/4
current_figure()
```
Same plot as in @fig-makie-observable with `h'` value updated
:::
The line `h[] = 1/4` updated `h` which then updated `points` (a points is lifted up from `h`) which updated the graphic. (In these notes, we replot to see the change, but in an interactive session, the current *displayed* figure would be updated; no replotting would be necessary.)
Finally, this example shows how to add a slider to adjust the value of `h` with a mouse. The slider object is positioned along with a label using the grid reference, as before.
::: {#fig-makie=slider-added}
```{julia}
let
f(x) = sqrt(x)
c = 1
xs = 0..3
F = Figure()
ax = Axis(F[1,1:2])
h = Slider(F[2,2], range = 0.01:0.01:1.5, startvalue = 1.5)
Label(F[2,1], "Adjust slider to change `h`";
justification = :left)
points = lift(h.value) do h
xs = [0,c,c+h,3]
tl = x-> f(c) + (f(c+h)-f(c))/h * (x-c)
[Point2(x, tl(x)) for x ∈ xs]
end
lines!(ax, xs, f)
lines!(ax, points)
scatter!(ax, points; markersize=10)
current_figure()
end
```
A slider added in position `F[2,2]` and connected through `lift`
:::
The slider value is "lifted" by its `value` component, as shown. Otherwise, the above is fairly similar to just using an observable for `h`.