use quarto, not Pluto to render pages

This commit is contained in:
jverzani
2022-07-24 16:38:24 -04:00
parent 93c993206a
commit 7b37ca828c
879 changed files with 793311 additions and 2678 deletions

View File

@@ -0,0 +1,9 @@
[deps]
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807"
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
MDBM = "dd61e66b-39ce-57b0-8813-509f78be4b4d"
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
Weave = "44d3d7a6-8a23-5bf8-98c5-b353f8df5ec9"

View File

@@ -0,0 +1,11 @@
# Alternatives
There are many ways to do related things in `Julia`. This directory holds alternatives to the some choices made within these notes:
## Symbolics
* needs writing
## Makie
* needs updating

View File

@@ -0,0 +1,107 @@
# Using interval arithemetic
Highlighted here is the use of interval arithmetic for calculus problems.
Unlike floating point math, where floating point values are an *approximation* to real numbers, interval arithmetic uses *interval* which are **guaranteed** to contain the given value. We use the `IntervalArithmetic` package and friends to work below, but note there is nothing magic about the concept.
## Basic XXX
## Using `IntervalRootFinding` to identify zeros of a function
The `IntervalRootFinding` package provides a more *rigorous* alternative to `find_zeros`. This packages leverages the interval arithmetic features of `IntervalArithmetic`.
The `IntervalRootFinding` package provides a function `roots`, with usage similar to `find_zeros`. Intervals are specified with the notation `a..b`. In the following, we *qualify* `roots` to not conflict with the `roots` function from `SymPy`, which has already been loaded:
```julia
import IntervalArithmetic
import IntervalRootFinding
```
```julia
u(x) = sin(x) - 0.1*x^2 + 1
𝑱 = IntervalArithmetic.Interval(-10, 10) # cumbersome -10..10; needed here: .. means something in CalculusWithJulia
rts = IntervalRootFinding.roots(u, 𝑱)
```
The "zeros" are returned with an enclosing interval and a flag, which for the above indicates a unique zero in the interval.
The intervals with a unique answer can be filtered and refined with a construct like the following:
```julia
[find_zero(u, (IntervalArithmetic.interval(I).lo, IntervalArithmetic.interval(I).hi)) for I in rts if I.status == :unique]
```
The midpoint of the returned interval can be found by composing the `mid` function with the `interval` function of the package:
```julia
[(IntervalArithmetic.mid ∘ IntervalArithmetic.interval)(I) for I in rts if I.status == :unique]
```
For some problems, `find_zeros` is more direct, as with this one:
```julia
find_zeros(u, (-10, 10))
```
Which can be useful if there is some prior understanding of the zeros expected to be found.
However, `IntervalRootFinding` is more efficient computationally and *offers a guarantee* on the values found.
For functions where roots are not "unique" a different output may appear:
```julia; hold=true;
f(x) = x*(x-1)^2
rts = IntervalRootFinding.roots(f, 𝑱)
```
The interval labeled `:unknown` contains a `0`, but it can't be proved by `roots`.
Interval arithmetic finds **rigorous** **bounds** on the range of `f` values over a closed interval `a..b` (the range is `f(a..b)`). "Rigorous" means the bounds are truthful and account for possible floating point issues. "Bounds" means the answer lies within, but the bound need not be the answer.
This allows one -- for some functions -- to answer affirmatively questions like:
* Is the function *always* positive on `a..b`? Negative? This can be done by checking if `0` is in the bound given by `f(a..b)`. If it isn't then one of the two characterizations is true.
* Is the function *strictly increasing* on `a..b`? Strictly decreasing? These questions can be answered using the (upcoming) [derivative](../derivatives/derivatives.html). If the derivative is positive on `a..b` then `f` is strictly increasing, if negative on `a..b` then `f` is strictly decreasing. Finding the derivative can be done within the `IntervalArithmetic` framework using [automatic differentiation](../derivatives/numeric_derivatives.html), a blackbox operation denoted `f'` below.
Combined, for some functions and some intervals these two questions can be answered affirmatively:
* the interval does not contain a zero (`0 !in f(a..b)`)
* over the interval, the function crosses the `x` axis *once* (`f(a..a)` and `f(b..b)` are one positive and one negative *and* `f` is strictly monotone, or `0 !in f'(a..b)`)
This allows the following (simplified) bisection-like algorithm to be used:
* consider an interval `a..b`
* if the function is *always* positive or negative, it can be discarded as no zero can be in the interval
* if the function crosses the `x` axis *once* over this interval **then** there is a "unique" zero in the interval and the interval can be marked so and set aside
* if neither of the above *and* `a..b` is not too small already, then *sub-divide* the interval and repeat the above with *both* smaller intervals
* if `a..b` is too small, stop and mark it as "unknown"
When terminated there will be intervals with unique zeros flagged and smaller intervals with an unknown status.
Compared to the *bisection* algorithm -- which only knows for some intervals if that interval has one or more crossings -- this algorithm gives a more rigorous means to get all the zeros in `a..b`.
For a last example of the value of this package, this function, which appeared in our discussion on limits, is *positive* for **every** floating point number, but has two zeros snuck in at values within the floating point neighbors of $15/11$
```julia
t(x) = x^2 + 1 +log(abs( 11*x-15 ))/99
```
The `find_zeros` function will fail on identifying any potential zeros of this function. Even the basic call of `roots` will fail, due to it relying on some smoothness of `f`. However, explicitly asking for `Bisection` shows the *potential* for one or more zeros near $15/11$:
```julia
IntervalRootFinding.roots(t, 𝑱, IntervalRootFinding.Bisection)
```
(The basic algorithm above can be sped up using a variant of [Newton's](../derivatives/newton_method.html) method, this variant assumes some "smoothness" in the function `f`, whereas this `f` is not continuous at the point ``x=15/11``.)

View File

@@ -0,0 +1,904 @@
# 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.

View File

@@ -0,0 +1,763 @@
# JavaScript based plotting libraries
```{julia}
#| echo: false
import Logging
Logging.disable_logging(Logging.Info) # or e.g. Logging.Info
Logging.disable_logging(Logging.Warn)
import SymPy
function Base.show(io::IO, ::MIME"text/html", x::T) where {T <: SymPy.SymbolicObject}
println(io, "<span class=\"math-left-align\" style=\"padding-left: 4px; width:0; float:left;\"> ")
println(io, "\\[")
println(io, sympy.latex(x))
println(io, "\\]")
println(io, "</span>")
end
# hack to work around issue
import Markdown
import CalculusWithJulia
function CalculusWithJulia.WeaveSupport.ImageFile(d::Symbol, f::AbstractString, caption; kwargs...)
nm = joinpath("..", string(d), f)
u = "![$caption]($nm)"
Markdown.parse(u)
end
nothing
```
This section uses this add-on package:
```{julia}
using PlotlyLight
```
To avoid a dependence on the `CalculusWithJulia` package, we load two utility packages:
```{julia}
using PlotUtils
using SplitApplyCombine
```
---
`Julia` has different interfaces to a few JavaScript plotting libraries, notably the [vega](https://vega.github.io/vega/) and [vega-lite](https://vega.github.io/vega-lite/) through the [VegaLite.jl](https://github.com/queryverse/VegaLite.jl) package, and [plotly](https://plotly.com/javascript/) through several interfaces: `Plots.jl`, `PlotlyJS.jl`, and `PlotlyLight.jl`. These all make web-based graphics, for display through a web browser.
The `Plots.jl` interface is a backend for the familiar `Plots` package, making the calling syntax familiar, as is used throughout these notes. The `plotly()` command, from `Plots`, switches to this backend.
The `PlotlyJS.jl` interface offers direct translation from `Julia` structures to the underlying `JSON` structures needed by plotly, and has mechanisms to call back into `Julia` from `JavaScript`. This allows complicated interfaces to be produced.
Here we discuss `PlotlyLight` which conveniently provides the translation from `Julia` structures to the `JSON` structures needed in a light-weight package, which plots quickly, without the delays due to compilation of the more complicated interfaces. Minor modifications would be needed to adjust the examples to work with `PlotlyJS` or `PlotlyBase`. The documentation for the `JavaScript` [library](https://plotly.com/javascript/) provides numerous examples which can easily be translated. The [one-page-reference](https://plotly.com/javascript/reference/) gives specific details, and is quoted from below, at times.
This discussion covers the basic of graphing for calculus purposes. It does not cover, for example, the faceting common in statistical usages, or the chart types common in business and statistics uses. The `plotly` library is much more extensive than what is reviewed below.
## Julia dictionaries to JSON
`PlotlyLight` uses the `JavaScript` interface for the `plotly` libraries. Unlike more developed interfaces, like the one for `Python`, `PlotlyLight` only manages the translation from `Julia` structures to `JavaScript` structures and the display of the results.
The key to translation is the mapping for `Julia`'s dictionaries to the nested `JSON` structures needed by the `JavaScript` library.
For example, an introductory [example](https://plotly.com/javascript/line-and-scatter/) for a scatter plot includes this `JSON` structure:
```{julia}
#| eval: false
var trace1 = {
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
mode: 'markers',
type: 'scatter'
};
```
The `{}` create a list, the `[]` an Array (or vector, as it does with `Julia`), the `name:` are keys. The above is simply translated via:
```{julia}
Config(x = [1,2,3,4],
y = [10, 15, 13, 17],
mode = "markers",
type = "scatter"
)
```
The `Config` constructor (from the `EasyConfig` package loaded with `PlotlyLight`) is an interface for a dictionary whose keys are symbols, which are produced by the named arguments passed to `Config`. By nesting `Config` statements, nested `JavaScript` structures can be built up. As well, these can be built on the fly using `.` notation, as in:
```{julia}
#| hold: true
cfg = Config()
cfg.key1.key2.key3 = "value"
cfg
```
To produce a figure with `PlotlyLight` then is fairly straightforward: data and, optionally, a layout are created using `Config`, then passed along to the `Plot` command producing a `Plot` object which has `display` methods defined for it. This will be illustrated through the examples.
## Scatter plot
A basic scatter plot of points $(x,y)$ is created as follows:
```{julia}
#| hold: true
xs = 1:5
ys = rand(5)
data = Config(x = xs,
y = ys,
type="scatter",
mode="markers"
)
Plot(data)
```
The symbols `x` and `y` (and later `z`) specify the data to `plotly`. Here the `mode` is specified to show markers.
The `type` key specifies the chart or trace type. The `mode` specification sets the drawing mode for the trace. Above it is "markers". It can be any combination of "lines", "markers", or "text" joined with a "+" if more than one is desired.
## Line plot
A line plot is very similar, save for a different `mode` specification:
```{julia}
#| hold: true
xs = 1:5
ys = rand(5)
data = Config(x = xs,
y = ys,
type="scatter",
mode="lines"
)
Plot(data)
```
The difference is solely the specification of the `mode` value, for a line plot it is "lines," for a scatter plot it is "markers" The `mode` "lines+markers" will plot both. The default for the "scatter" types is to use "lines+markers" for small data sets, and "lines" for others, so for this example, `mode` could be left off.
### Nothing
The line graph plays connect-the-dots with the points specified by paired `x` and `y` values. *Typically*, when and `x` value is `NaN` that "dot" (or point) is skipped. However, `NaN` doesn't pass through the JSON conversion `nothing` can be used.
```{julia}
#| hold: true
data = Config(
x=[0,1,nothing,3,4,5],
y = [0,1,2,3,4,5],
type="scatter", mode="markers+lines")
Plot(data)
```
## Multiple plots
More than one graph or layer can appear on a plot. The `data` argument can be a vector of `Config` values, each describing a plot. For example, here we make a scatter plot and a line plot:
```{julia}
#| hold: true
data = [Config(x = 1:5,
y = rand(5),
type = "scatter",
mode = "markers",
name = "scatter plot"),
Config(x = 1:5,
y = rand(5),
type = "scatter",
mode = "lines",
name = "line plot")
]
Plot(data)
```
The `name` argument adjusts the name in the legend referencing the plot. This is produced by default.
### Adding a layer
In `PlotlyLight`, the `Plot` object has a field `data` for storing a vector of configurations, as above. After a plot is made, this field can have values pushed onto it and the corresponding layers will be rendered when the plot is redisplayed.
For example, here we plot the graphs of both the $\sin(x)$ and $\cos(x)$ over $[0,2\pi]$. We used the utility `PlotUtils.adapted_grid` to select the points to use for the graph.
```{julia}
#| hold: true
a, b = 0, 2pi
xs, ys = PlotUtils.adapted_grid(sin, (a,b))
p = Plot(Config(x=xs, y=ys, name="sin"))
xs, ys = PlotUtils.adapted_grid(cos, (a,b))
push!(p.data, Config(x=xs, y=ys, name="cos"))
p # to display the plot
```
The values for `a` and `b` are used to generate the $x$- and $y$-values. These can also be gathered from the existing plot object. Here is one way, where for each trace with an `x` key, the extrema are consulted to update a list of left and right ranges.
```{julia}
#| hold: true
xs, ys = PlotUtils.adapted_grid(x -> x^5 - x - 1, (0, 2)) # answer is (0,2)
p = Plot([Config(x=xs, y=ys, name="Polynomial"),
Config(x=xs, y=0 .* ys, name="x-axis", mode="lines", line=Config(width=5))]
)
ds = filter(d -> !isnothing(get(d, :x, nothing)), p.data)
a=reduce(min, [minimum(d.x) for d ∈ ds]; init=Inf)
b=reduce(max, [maximum(d.x) for d ∈ ds]; init=-Inf)
(a, b)
```
## Interactivity
`JavaScript` allows interaction with a plot as it is presented within a browser. (Not the `Julia` process which produced the data or the plot. For that interaction, `PlotlyJS` may be used.) The basic *default* features are:
* The data producing a graphic are displayed on hover using flags.
* The legend may be clicked to toggle whether the corresponding graph is displayed.
* The viewing region can be narrowed using the mouse for selection.
* The toolbar has several features for panning and zooming, as well as adjusting the information shown on hover.
Later we will see that $3$-dimensional surfaces can be rotated interactively.
## Plot attributes
Attributes of the markers and lines may be adjusted when the data configuration is specified. A selection is shown below. Consult the reference for the extensive list.
### Marker attributes
A marker's attributes can be adjusted by values passed to the `marker` key. Labels for each marker can be assigned through a `text` key and adding `text` to the `mode` key. For example:
```{julia}
#| hold: true
data = Config(x = 1:5,
y = rand(5),
mode="markers+text",
type="scatter",
name="scatter plot",
text = ["marker $i" for i in 1:5],
textposition = "top center",
marker = Config(size=12, color=:blue)
)
Plot(data)
```
The `text` mode specification is necessary to have text be displayed on the chart, and not just appear on hover. The `size` and `color` attributes are recycled; they can be specified using a vector for per-marker styling. Here the symbol `:blue` is used to specify a color, which could also be a name, such as `"blue"`.
#### RGB Colors
The `ColorTypes` package is the standard `Julia` package providing an `RGB` type (among others) for specifying red-green-blue colors. To make this work with `Config` and `JSON3` requires some type-piracy (modifying `Base.string` for the `RGB` type) to get, say, `RGB(0.5, 0.5, 0.5)` to output as `"rgb(0.5, 0.5, 0.5)"`. (RGB values in JavaScript are integers between $0$ and $255$ or floating point values between $0$ and $1$.) A string with this content can be specified. Otherwise, something like the following can be used to avoid the type piracy:
```{julia}
struct rgb
r
g
b
end
PlotlyLight.JSON3.StructTypes.StructType(::Type{rgb}) = PlotlyLight.JSON3.StructTypes.StringType()
Base.string(x::rgb) = "rgb($(x.r), $(x.g), $(x.b))"
```
With these defined, red-green-blue values can be used for colors. For example to give a range of colors, we might have:
```{julia}
#| hold: true
cols = [rgb(i,i,i) for i in range(10, 245, length=5)]
sizes = [12, 16, 20, 24, 28]
data = Config(x = 1:5,
y = rand(5),
mode="markers+text",
type="scatter",
name="scatter plot",
text = ["marker $i" for i in 1:5],
textposition = "top center",
marker = Config(size=sizes, color=cols)
)
Plot(data)
```
The `opacity` key can be used to control the transparency, with a value between $0$ and $1$.
#### Marker symbols
The `marker_symbol` key can be used to set a marker shape, with the basic values being: `circle`, `square`, `diamond`, `cross`, `x`, `triangle`, `pentagon`, `hexagram`, `star`, `diamond`, `hourglass`, `bowtie`, `asterisk`, `hash`, `y`, and `line`. Add `-open` or `-open-dot` modifies the basic shape.
```{julia}
#| hold: true
markers = ["circle", "square", "diamond", "cross", "x", "triangle", "pentagon",
"hexagram", "star", "diamond", "hourglass", "bowtie", "asterisk",
"hash", "y", "line"]
n = length(markers)
data = [Config(x=1:n, y=1:n, mode="markers",
marker = Config(symbol=markers, size=10)),
Config(x=1:n, y=2 .+ (1:n), mode="markers",
marker = Config(symbol=markers .* "-open", size=10)),
Config(x=1:n, y=4 .+ (1:n), mode="markers",
marker = Config(symbol=markers .* "-open-dot", size=10))
]
Plot(data)
```
### Line attributes
The `line` key can be used to specify line attributes, such as `width` (pixel width), `color`, or `dash`.
The `width` key specifies the line width in pixels.
The `color` key specifies the color of the line drawn.
The `dash` key specifies the style for the drawn line. Values can be set by string from "solid", "dot", "dash", "longdash", "dashdot", or "longdashdot" or set by specifying a pattern in pixels, e.g. "5px,10px,2px,2px".
The `shape` attribute determine how the points are connected. The default is `linear`, but other possibilities are `hv`, `vh`, `hvh`, `vhv`, `spline` for various patterns of connectivity. The following example, from the plotly documentation, shows the differences:
```{julia}
#| hold: true
shapes = ["linear", "hv", "vh", "hvh", "vhv", "spline"]
data = [Config(x = 1:5, y = 5*(i-1) .+ [1,3,2,3,1], mode="lines+markers", type="scatter",
name=shape,
line=Config(shape=shape)
) for (i, shape) ∈ enumerate(shapes)]
Plot(data)
```
### Text
The text associated with each point can be drawn on the chart, when "text" is included in the `mode` or shown on hover.
The onscreen text is passed to the `text` attribute. The [`texttemplate`](https://plotly.com/javascript/reference/scatter/#scatter-texttemplate) key can be used to format the text with details in the accompanying link.
Similarly, the `hovertext` key specifies the text shown on hover, with [`hovertemplate`](https://plotly.com/javascript/reference/scatter/#scatter-hovertemplate) used to format the displayed text.
### Filled regions
The `fill` key for a chart of mode `line` specifies how the area around a chart should be colored, or filled. The specification are declarative, with values in "none", "tozeroy", "tozerox", "tonexty", "tonextx", "toself", and "tonext". The value of "none" is the default, unless stacked traces are used.
In the following, to highlight the difference between $f(x) = \cos(x)$ and $p(x) = 1 - x^2/2$ the area from $f$ to the next $y$ is declared; for $p$, the area to $0$ is declared.
```{julia}
#| hold: true
xs = range(-1, 1, 100)
data = [
Config(
x=xs, y=cos.(xs),
fill = "tonexty",
fillcolor = "rgba(0,0,255,0.25)", # to get transparency
line = Config(color=:blue)
),
Config(
x=xs, y=[1 - x^2/2 for x ∈ xs ],
fill = "tozeroy",
fillcolor = "rgba(255,0,0,0.25)", # to get transparency
line = Config(color=:red)
)
]
Plot(data)
```
The `toself` declaration is used below to fill in a polygon:
```{julia}
#| hold: true
data = Config(
x=[-1,1,1,-1,-1], y = [-1,1,-1,1,-1],
fill="toself",
type="scatter")
Plot(data)
```
## Layout attributes
The `title` key sets the main title; the `title` key in the `xaxis` configuration sets the $x$-axis title (similarly for the $y$ axis).
The legend is shown when $2$ or more charts or specified, by default. This can be adjusted with the `showlegend` key, as below. The legend shows the corresponding `name` for each chart.
```{julia}
#| hold: true
data = Config(x=1:5, y=rand(5), type="scatter", mode="markers", name="legend label")
lyt = Config(title = "Main chart title",
xaxis = Config(title="x-axis label"),
yaxis = Config(title="y-axis label"),
showlegend=true
)
Plot(data, lyt)
```
The `xaxis` and `yaxis` keys have many customizations. For example: `nticks` specifies the maximum number of ticks; `range` to set the range of the axis; `type` to specify the axis type from "linear", "log", "date", "category", or "multicategory;" and `visible`
The aspect ratio of the chart can be set to be equal through the `scaleanchor` key, which specifies another axis to take a value from. For example, here is a parametric plot of a circle:
```{julia}
#| hold: true
ts = range(0, 2pi, length=100)
data = Config(x = sin.(ts), y = cos.(ts), mode="lines", type="scatter")
lyt = Config(title = "A circle",
xaxis = Config(title = "x"),
yaxis = Config(title = "y",
scaleanchor = "x")
)
Plot(data, lyt)
```
#### Annotations
Text annotations may be specified as part of the layout object. Annotations may or may not show an arrow. Here is a simple example using a vector of annotations.
```{julia}
#| hold: true
data = Config(x = [0, 1], y = [0, 1], mode="markers", type="scatter")
layout = Config(title = "Annotations",
xaxis = Config(title="x",
range = (-0.5, 1.5)),
yaxis = Config(title="y",
range = (-0.5, 1.5)),
annotations = [
Config(x=0, y=0, text = "(0,0)"),
Config(x=1, y=1.2, text = "(1,1)", showarrow=false)
]
)
Plot(data, layout)
```
The following example is more complicated use of the elements previously described. It mimics an image from [Wikipedia](https://en.wikipedia.org/wiki/List_of_trigonometric_identities) for trigonometric identities. The use of $\LaTeX$ does not seem to be supported through the `JavaScript` interface; unicode symbols are used instead. The `xanchor` and `yanchor` keys are used to position annotations away from the default. The `textangle` key is used to rotate text, as desired.
```{julia, hold=true}
alpha = pi/6
beta = pi/5
xₘ = cos(alpha)*cos(beta)
yₘ = sin(alpha+beta)
r₀ = 0.1
data = [
Config(
x = [0,xₘ, xₘ, 0, 0],
y = [0, 0, yₘ, yₘ, 0],
type="scatter", mode="line"
),
Config(
x = [0, xₘ],
y = [0, sin(alpha)*cos(beta)],
fill = "tozeroy",
fillcolor = "rgba(100, 100, 100, 0.5)"
),
Config(
x = [0, cos(alpha+beta), xₘ],
y = [0, yₘ, sin(alpha)*cos(beta)],
fill = "tonexty",
fillcolor = "rgba(200, 0, 100, 0.5)",
),
Config(
x = [0, cos(alpha+beta)],
y = [0, yₘ],
line = Config(width=5, color=:black)
)
]
lyt = Config(
height=450,
showlegend=false,
xaxis=Config(visible=false),
yaxis = Config(visible=false, scaleanchor="x"),
annotations = [
Config(x = r₀*cos(alpha/2), y = r₀*sin(alpha/2),
text="α", showarrow=false),
Config(x = r₀*cos(alpha+beta/2), y = r₀*sin(alpha+beta/2),
text="β", showarrow=false),
Config(x = cos(alpha+beta) + r₀*cos(pi+(alpha+beta)/2),
y = yₘ + r₀*sin(pi+(alpha+beta)/2),
xanchor="center", yanchor="center",
text="α+β", showarrow=false),
Config(x = xₘ + r₀*cos(pi/2+alpha/2),
y = sin(alpha)*cos(beta) + r₀ * sin(pi/2 + alpha/2),
text="α", showarrow=false),
Config(x = 1/2 * cos(alpha+beta),
y = 1/2 * sin(alpha+beta),
text = "1"),
Config(x = xₘ/2*cos(alpha), y = xₘ/2*sin(alpha),
xanchor="center", yanchor="bottom",
text = "cos(β)",
textangle=-rad2deg(alpha),
showarrow=false),
Config(x = xₘ + sin(beta)/2*cos(pi/2 + alpha),
y = sin(alpha)*cos(beta) + sin(beta)/2*sin(pi/2 + alpha),
xanchor="center", yanchor="top",
text = "sin(β)",
textangle = rad2deg(pi/2-alpha),
showarrow=false),
Config(x = xₘ/2,
y = 0,
xanchor="center", yanchor="top",
text = "cos(α)⋅cos(β)", showarrow=false),
Config(x = 0,
y = yₘ/2,
xanchor="right", yanchor="center",
text = "sin(α+β)",
textangle=-90,
showarrow=false),
Config(x = cos(alpha+beta)/2,
y = yₘ,
xanchor="center", yanchor="bottom",
text = "cos(α+β)", showarrow=false),
Config(x = cos(alpha+beta) + (xₘ - cos(alpha+beta))/2,
y = yₘ,
xanchor="center", yanchor="bottom",
text = "sin(α)⋅sin(β)", showarrow=false),
Config(x = xₘ, y=sin(alpha)*cos(beta) + (yₘ - sin(alpha)*cos(beta))/2,
xanchor="left", yanchor="center",
text = "cos(α)⋅sin(β)",
textangle=90,
showarrow=false),
Config(x = xₘ,
y = sin(alpha)*cos(beta)/2,
xanchor="left", yanchor="center",
text = "sin(α)⋅cos(β)",
textangle=90,
showarrow=false)
]
)
Plot(data, lyt)
```
## Parameterized curves
In $2$-dimensions, the plotting of a parameterized curve is similar to that of plotting a function. In $3$-dimensions, an extra $z$-coordinate is included.
To help, we define an `unzip` function as an interface to `SplitApplyCombine`'s `invert` function:
```{julia}
unzip(v) = SplitApplyCombine.invert(v)
```
Earlier, we plotted a two dimensional circle, here we plot the related helix.
```{julia}
#| hold: true
helix(t) = [cos(t), sin(t), t]
ts = range(0, 4pi, length=200)
xs, ys, zs = unzip(helix.(ts))
data = Config(x=xs, y=ys, z=zs,
type = "scatter3d", # <<- note the 3d
mode = "lines",
line=(width=2,
color=:red)
)
Plot(data)
```
The main difference is the chart type, as this is a $3$-dimensional plot, "scatter3d" is used.
### Quiver plots
There is no `quiver` plot for `plotly` using JavaScript. In $2$-dimensions a text-less annotation could be employed. In $3$-dimensions, the following (from [stackoverflow.com](https://stackoverflow.com/questions/43164909/plotlypython-how-to-plot-arrows-in-3d)) is a possible workaround where a line segment is drawn and capped with a small cone. Somewhat opaquely, we use `NamedTuple` for an iterator to create the keys for the data below:
```{julia}
#| hold: true
helix(t) = [cos(t), sin(t), t]
helix(t) = [-sin(t), cos(t), 1]
ts = range(0, 4pi, length=200)
xs, ys, zs = unzip(helix.(ts))
helix_trace = Config(; NamedTuple(zip((:x,:y,:z), unzip(helix.(ts))))...,
type = "scatter3d", # <<- note the 3d
mode = "lines",
line=(width=2,
color=:red)
)
tss = pi/2:pi/2:7pi/2
rs, rs = helix.(tss), helix.(tss)
arrows = [
Config(x = [x[1], x[1]+x[1]],
y = [x[2], x[2]+x[2]],
z = [x[3], x[3]+x[3]],
mode="lines", type="scatter3d")
for (x, x) ∈ zip(rs, rs)
]
tips = rs .+ rs
lengths = 0.1 * rs
caps = Config(;
NamedTuple(zip([:x,:y,:z], unzip(tips)))...,
NamedTuple(zip([:u,:v,:w], unzip(lengths)))...,
type="cone", anchor="tail")
data = vcat(helix_trace, arrows, caps)
Plot(data)
```
If several arrows are to be drawn, it might be more efficient to pass multiple values in for the `x`, `y`, ... values. They expect a vector. In the above, we create $1$-element vectors.
## Contour plots
A contour plot is created by the "contour" trace type. The data is prepared as a vector of vectors, not a matrix. The following has the interior vector corresponding to slices ranging over $x$ for a fixed $y$. With this, the construction is straightforward using a comprehension:
```{julia}
#| hold: true
f(x,y) = x^2 - 2y^2
xs = range(0,2,length=25)
ys = range(0,2, length=50)
zs = [[f(x,y) for x in xs] for y in ys]
data = Config(
x=xs, y=ys, z=zs,
type="contour"
)
Plot(data)
```
The same `zs` data can be achieved by broadcasting and then collecting as follows:
```{julia}
#| hold: true
f(x,y) = x^2 - 2y^2
xs = range(0,2,length=25)
ys = range(0,2, length=50)
zs = collect(eachrow(f.(xs', ys)))
data = Config(
x=xs, y=ys, z=zs,
type="contour"
)
Plot(data)
```
The use of just `f.(xs', ys)` or `f.(xs, ys')`, as with other plotting packages, is not effective, as `JSON3` writes matrices as vectors (with linear indexing).
## Surface plots
The chart type "surface" allows surfaces in $3$ dimensions to be plotted.
### Surfaces defined by $z = f(x,y)$
Surfaces defined through a scalar-valued function are drawn quite naturally, save for needing to express the height data ($z$ axis) using a vector of vectors, and not a matrix.
```{julia}
#| hold: true
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 = range(-3,3, length=50)
ys = range(-3,3, length=50)
zs = [[peaks(x,y) for x in xs] for y in ys]
data = Config(x=xs, y=ys, z=zs,
type="surface")
Plot(data)
```
### Parametrically defined surfaces
For parametrically defined surfaces, the $x$ and $y$ values also correspond to matrices. Her we see a pattern to plot a torus. The [`aspectmode`](https://plotly.com/javascript/reference/layout/scene/#layout-scene-aspectmode) instructs the scene's axes to be drawn in proportion with the axes' ranges.
```{julia}
#| hold: true
r, R = 1, 5
X(theta,phi) = [(r*cos(theta)+R)*cos(phi), (r*cos(theta)+R)*sin(phi), r*sin(theta)]
us = range(0, 2pi, length=25)
vs = range(0, pi, length=25)
xs = [[X(u,v)[1] for u in us] for v in vs]
ys = [[X(u,v)[2] for u in us] for v in vs]
zs = [[X(u,v)[3] for u in us] for v in vs]
data = Config(
x = xs, y = ys, z = zs,
type="surface",
mode="scatter3d"
)
lyt = Config(scene=Config(aspectmode="data"))
Plot(data, lyt)
```

View File

@@ -0,0 +1,10 @@
# seems like with
* quadrature (renamed)
* nonlinsolve
* GalacticOptim (renamed)
* symbolic-numeric integration
* symbolics.jl
...
This should be mentioned

View File

@@ -0,0 +1,392 @@
# Symbolics.jl
Incorporate:
Basics
https://github.com/SciML/ModelingToolkit.jl
https://github.com/JuliaSymbolics/Symbolics.jl
https://github.com/JuliaSymbolics/SymbolicUtils.jl
* Rewriting
https://github.com/JuliaSymbolics/SymbolicUtils.jl
* Plotting
Polynomials
Limits
XXX ... room here!
Derivatives
https://github.com/JuliaSymbolics/Symbolics.jl
Integration
https://github.com/SciML/SymbolicNumericIntegration.jl
The `Symbolics.jl` package is a Computer Algebra System (CAS) built entirely in `Julia`.
This package is under heavy development.
## Algebraic manipulations
### construction
@variables
SymbolicUtils.@syms assumptions
x is a `Num`, `Symbolics.value(x)` is of type `SymbolicUtils{Real, Nothing}
relation to SymbolicUtils
Num wraps things; Term
### Substitute
### Simplify
simplify
expand
rewrite rules
### Solving equations
solve_for
## Expressions to functions
build_function
## Derivatives
1->1: Symbolics.derivative(x^2 + cos(x), x)
1->3: Symbolics.derivative.([x^2, x, cos(x)], x)
3 -> 1: Symbolics.gradient(x*y^z, [x,y,z])
2 -> 2: Symbolics.jacobian([x,y^z], [x,y])
# higher order
1 -> 1: D(ex, x, n=1) = foldl((ex,_) -> Symbolics.derivative(ex, x), 1:n, init=ex)
2 -> 1: (2nd) Hessian
## Differential equations
## Integrals
WIP
## ----
# follow sympy tutorial
using Symbolics
import SymbolicUtils
@variables x y z
# substitution
ex = cos(x) + 1
substitute(ex, Dict(x=>y))
substitute(ex, Dict(x=>0)) # does eval
ex = x^y
substitute(ex, Dict(y=> x^y))
# expand trig
r1 = @rule sin(2 * ~x) => 2sin(~x)*cos(~x)
r2 = @rule cos(2 * ~x) => cos(~x)^2 - sin(~x)^2
expand_trig(ex) = simplify(ex, RuleSet([r1, r2]))
ex = sin(2x) + cos(2x)
expand_trig(ex)
## Multiple
@variables x y z
ex = x^3 + 4x*y -z
substitute(ex, Dict(x=>2, y=>4, z=>0))
# Converting Strings to Expressions
# what is sympify?
# evalf
# lambdify: symbolic expression -> function
ex = x^3 + 4x*y -z
λ = build_function(ex, x,y,z, expression=Val(false))
λ(2,4,0)
# pretty printing
using Latexify
latexify(ex)
# Simplify
@variables x y z t
simplify(sin(x)^2 + cos(x)^2)
simplify((x^3 + x^2 - x - 1) / (x^2 + 2x + 1)) # fails, no factor
simplify(((x+1)*(x^2-1))/((x+1)^2)) # works
import SpecialFunctions: gamma
simplify(gamma(x) / gamma(x-2)) # fails
# Polynomial
## expand
expand((x+1)^2)
expand((x+2)*(x-3))
expand((x+1)*(x-2) - (x-1)*x)
## factor
### not defined
## collect
COLLECT_RULES = [
@rule(~x*x^(~n::SymbolicUtils.isnonnegint) => (~x, ~n))
@rule(~x * x => (~x, 1))
]
function _collect(ex, x)
d = Dict()
exs = expand(ex)
if SymbolicUtils.operation(Symbolics.value(ex)) != +
d[0] => ex
else
for aᵢ ∈ SymbolicUtils.arguments(Symbolics.value(expand(ex)))
u = simplify(aᵢ, RuleSet(COLLECT_RULES))
if isa(u, Tuple)
a,n = u
else
a,n = u,0
end
d[n] = get(d, n, 0) + a
end
end
d
end
## cancel -- no factor
## apart -- no factor
## Trignometric simplification
INVERSE_TRIG_RUELS = [@rule(cos(acos(~x)) => ~x)
@rule(acos(cos(~x)) => abs(rem2pi(~x, RoundNearest)))
@rule(sin(asin(~x)) => ~x)
@rule(asin(sin(~x)) => abs(rem2pi(x + pi/2, RoundNearest)) - pi/2)
]
@variables θ
simplify(cos(acos(θ)), RuleSet(INVERSE_TRIG_RUELS))
# Copy from https://github.com/JuliaSymbolics/SymbolicUtils.jl/blob/master/src/simplify_rules.jl
# the TRIG_RULES are applied by simplify by default
HTRIG_RULES = [
@acrule(-sinh(~x)^2 + cosh(~x)^2 => one(~x))
@acrule(sinh(~x)^2 + 1 => cosh(~x)^2)
@acrule(cosh(~x)^2 + -1 => -sinh(~x)^2)
@acrule(tanh(~x)^2 + 1*sech(~x)^2 => one(~x))
@acrule(-tanh(~x)^2 + 1 => sech(~x)^2)
@acrule(sech(~x)^2 + -1 => -tanh(~x)^2)
@acrule(coth(~x)^2 + -1*csch(~x)^2 => one(~x))
@acrule(coth(~x)^2 + -1 => csch(~x)^2)
@acrule(csch(~x)^2 + 1 => coth(~x)^2)
@acrule(tanh(~x) => sinh(~x)/cosh(~x))
@acrule(sinh(-~x) => -sinh(~x))
@acrule(cosh(-~x) => -cosh(~x))
]
trigsimp(ex) = simplify(simplify(ex, RuleSet(HTRIG_RULES)))
trigsimp(sin(x)^2 + cos(x)^2)
trigsimp(sin(x)^4 -2cos(x)^2*sin(x)^2 + cos(x)^4) # no factor
trigsimp(cosh(x)^2 + sinh(x)^2)
trigsimp(sinh(x)/tanh(x))
EXPAND_TRIG_RULES = [
@acrule(sin(~x+~y) => sin(~x)*cos(~y) + cos(~x)*sin(~y))
@acrule(sinh(~x+~y) => sinh(~x)*cosh(~y) + cosh(~x)*sinh(~y))
@acrule(sin(2*~x) => 2sin(~x)*cos(~x))
@acrule(sinh(2*~x) => 2sinh(~x)*cosh(~x))
@acrule(cos(~x+~y) => cos(~x)*cos(~y) - sin(~x)*sin(~y))
@acrule(cosh(~x+~y) => cosh(~x)*cosh(~y) + sinh(~x)*sinh(~y))
@acrule(cos(2*~x) => cos(~x)^2 - sin(~x)^2)
@acrule(cosh(2*~x) => cosh(~x)^2 + sinh(~x)^2)
@acrule(tan(~x+~y) => (tan(~x) - tan(~y)) / (1 + tan(~x)*tan(~y)))
@acrule(tanh(~x+~y) => (tanh(~x) + tanh(~y)) / (1 + tanh(~x)*tanh(~y)))
@acrule(tan(2*~x) => 2*tan(~x)/(1 - tan(~x)^2))
@acrule(tanh(2*~x) => 2*tanh(~x)/(1 + tanh(~x)^2))
]
expandtrig(ex) = simplify(simplify(ex, RuleSet(EXPAND_TRIG_RULES)))
expandtrig(sin(x+y))
expandtrig(tan(2x))
# powers
# in genearl x^a*x^b = x^(a+b)
@variables x y a b
simplify(x^a*x^b - x^(a+b)) # 0
# x^a*y^a = (xy)^a When x,y >=0, a ∈ R
simplify(x^a*y^a - (x*y)^a)
## ??? How to specify such assumptions?
# (x^a)^b = x^(ab) only if b ∈ Int
@syms x a b
simplify((x^a)^b - x^(a*b))
@syms x a b::Int
simplify((x^a)^b - x^(a*b)) # nope
ispositive(x) = isa(x, Real) && x > 0
_isinteger(x) = isa(x, Integer)
_isinteger(x::SymbolicUtils.Sym{T,S}) where {T <: Integer, S} = true
POWSIMP_RULES = [
@acrule((~x::ispositive)^(~a::isreal) * (~y::ispositive)^(~a::isreal) => (~x*~y)^~a)
@rule(((~x)^(~a))^(~b::_isinteger) => ~x^(~a * ~b))
]
powsimp(ex) = simplify(simplify(ex, RuleSet(POWSIMP_RULES)))
@syms x a b::Int
simplify((x^a)^b - x^(a*b)) # nope
EXPAND_POWER_RULES = [
@rule((~x)^(~a + ~b) => (_~)^(~a) * (~x)^(~b))
@rule((~x*~y)^(~a) => (~x)^(~a) * (~y)^(~a))
## ... more on simplification...
## Calculus
@variables x y z
import Symbolics: derivative
derivative(cos(x), x)
derivative(exp(x^2), x)
# multiple derivative
Symbolics.derivative(ex, x, n::Int) = reduce((ex,_) -> derivative(ex, x), 1:n, init=ex) # helper
derivative(x^4, x, 3)
ex = exp(x*y*z)
using Chain
@chain ex begin
derivative(x, 3)
derivative(y, 3)
derivative(z, 3)
end
# using Differential operator
expr = exp(x*y*z)
expr |> Differential(x)^2 |> Differential(y)^3 |> expand_derivatives
# no integrate
# no limit
# Series
function series(ex, x, x0=0, n=5)
Σ = zero(ex)
for i ∈ 0:n
ex = expand_derivatives((Differential(x))(ex))
Σ += substitute(ex, Dict(x=>0)) * x^i / factorial(i)
end
Σ
end
# finite differences
# Solvers
@variables x y z a
eq = x ~ a
Symbolics.solve_for(eq, x)
eqs = [x + y + z ~ 1
x + y + 2z ~ 3
x + 2y + 3z ~ 3
]
vars = [x,y,z]
xs = Symbolics.solve_for(eqs, vars)
[reduce((ex, r)->substitute(ex, r), Pair.(vars, xs), init=ex.lhs) for ex ∈ eqs] == [eq.rhs for eq ∈ eqs]
A = [1 1; 1 2]
b = [1, 3]
xs = Symbolics.solve_for(A*[x,y] .~ b, [x,y])
A*xs - b
A = [1 1 1; 1 1 2]
b = [1,3]
A*[x,y,z] - b
Symbolics.solve_for(A*[x,y,z] .~ b, [x,y,z]) # fails, singular
# nonlinear solve
# use `λ = mk_function(ex, args, expression=Val(false))`
# polynomial roots
# differential equations