work on better figures

This commit is contained in:
jverzani
2025-07-02 06:25:10 -04:00
parent 50cc2b2193
commit 5013211954
12 changed files with 1098 additions and 61 deletions

View File

@@ -42,6 +42,101 @@ For these examples, the domain of both $f(x)$ and $g(x)$ is all real values of $
In general the range is harder to identify than the domain, and this is the case for these functions too. For $f(x)$ we may know the $\cos$ function is trapped in $[-1,1]$ and it is intuitively clear than all values in that set are possible. The function $h(x)$ would have range $[0,\infty)$. The $s(x)$ function is either $-1$ or $1$, so only has two possible values in its range. What about $g(x)$? It is a parabola that opens upward, so any $y$ values below the $y$ value of its vertex will not appear in the range. In this case, the symmetry indicates that the vertex will be at $(1/2, -1/4)$, so the range is $[-1/4, \infty)$.
::: {#fig-domain-range layout-nrow=2}
```{julia}
#| echo: false
plt = let
gr()
# domain/range shade
λ = 1.2
a, b = .1, 3
f(x) = (x-1/2) + sin((x-1)^2)
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35))
range_style = (; fill=(:blue, 0.35))
xs = range(a,b, 1000)
y0,y1 = extrema(f.(xs))
Δy = (y1-y0)/60
Δx = (b - a)/75
plot(; aspect_ratio=:equal, empty_style...)
plot!([-.25,3.25],[0,0]; axis_style...)
plot!([0,0], [min(-2Δy, y0 - Δy), y1 + 4Δy]; axis_style... )
plot!(f, a, b; fn_style...)
plot!(Shape([a,b,b,a], Δy * [-1,-1,1,1] ); domain_style...)
plot!(Shape(Δx*[-1,1,1,-1], [y0,y0,y1,y1]); range_style...)
plot!([a,a], [0, f(a)]; mark_style...)
plot!([b,b], [0, f(b)]; mark_style...)
plot!([a, b], [f(a), f(a)]; mark_style...)
plot!([a, b], [f(b), f(b)]; mark_style...)
end
plot
```
```{julia}
#| echo: false
plt = let
a, b = 0, 2pi
λ = 1.1
Δx, Δy = .033, .1
δx = 0.05
f(x) = sec(x)
g(x) = abs(f(x)) < 5 ? f(x) : NaN
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35))
range_style = (; fill=(:blue, 0.35))
plot(; empty_style...)
plot!(g, a+0.1, b; fn_style...)
plot!(λ*[a,b],[0,0]; axis_style...)
plot!([0,0], λ*[-5,5+1/2]; axis_style...)
vline!([pi/2, 3pi/2]; line=(:gray, :dash))
plot!(Shape([a,a+pi/2-δx,a+pi/2-δx,a], Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape([a+pi/2+δx, a+pi/2+pi-δx,a+pi/2+pi-δx,a+pi/2+δx],
Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape([3pi/2 + δx, 2pi, 2pi, 3pi/2+δx],
Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape(Δx*[-1,1,1,-1], [-5, -5,-1,-1]); range_style...)
plot!(Shape(Δx*[-1,1,1,-1], [ 5, 5,1,1]); range_style...)
end
plot
```
```{julia}
#| echo: false
plotly()
nothing
```
The top figure shows the domain and range of the function as highlighted intervals. The bottom figure shows that the domain may be a collection of intervals. (In this case the $\sec$ function is not defined at $\pi/2 + k \pi$ for integer $k$) and the range may be a collection of intervals. (In this case, the $\sec$ function never have a value in $(-1,1)$.
:::
:::{.callout-note}
## Note
@@ -90,7 +185,7 @@ Whereas function definitions and usage in `Julia` mirrors standard math notation
:::
### The domain of a function
### The domain of a function in Julia
Functions in `Julia` have an implicit domain, just as they do mathematically. In the case of $f(x)$ and $g(x)$, the right-hand side is defined for all real values of $x$, so the domain is all $x$. For $h(x)$ this isn't the case, of course. Trying to call $h(x)$ when $x < 0$ will give an error:
@@ -347,10 +442,27 @@ In our example, we see that in trying to find an answer to $f(x) = 0$ ( $\sqrt{2
```{julia}
#| echo: false
plot(q, a, b, linewidth=5, legend=false)
plot!(zero, a, b)
plot!([a, b], q.([a, b]))
scatter!([c], [q(c)])
plt = let
gr()
plt = plot(q, a, b, linewidth=5, legend=false)
plot!(plt, zero, a, b)
plot!(plt, [a, b], q.([a, b]))
scatter!(plt, [c], [q(c)]; marker=(:circle,))
scatter!(plt, [a,b], [q(a), q(b)]; marker=(:square,))
annotate!(plt, [
(a, 0, text(L"a", 10,:top)),
(b, 0, text(L"b", 10, :top)),
(c, 0, text(L"c", 10, :bottom))
])
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Still, `q(c)` is not really close to $0$:
@@ -398,6 +510,7 @@ c = secant_intersection(f, a, b)
p = plot(f, a, b, linewidth=5, legend=false)
plot!(p, zero, a, b)
scatter!([a,b], [f(a), f(b)]; marker=(:square,))
plot!(p, [a,b], f.([a,b]));
scatter!(p, [c], [f(c)])
@@ -918,6 +1031,86 @@ answ = 1
radioq(choices, answ)
```
##### Question
::: {#fig-floor-function}
```{julia}
#| echo: false
plt = let
gr()
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
ts = range(0, 2pi, 100)
xys = sincos.(ts)
xys = [.1 .* xy for xy in xys]
plot(; empty_style..., aspect_ratio=:equal)
plot!([-4.25,4.25], [0,0]; axis_style...)
plot!([0,0], [-4.25, 4.25]; axis_style...)
for k in -4:4
P,Q = (k,k),(k+1,k)
plot!([P,Q], line=(:black,1))
S = Shape([k .+ xy for xy in xys])
plot!(S; fill=(:black,))
S = Shape([(k+1,k) .+ xy for xy in xys])
plot!(S; fill=(:white,), line=(:black,1))
end
current()
end
plotly()
plt
```
The `floor` function rounds down. For example, any value in $[k,k+1)$ rounds to $k$ for integer $k$.
:::
The figure shows the `floor` function which is useful in programming. It rounds down to the first integer value.
From the graph, what is the domain of the function?
```{julia}
#| hold: true
#| echo: false
choices = [
"The entire real line",
"The entire real line except for integer values",
"The integers"
]
answer = 1
radioq(choices, answ)
```
From the graph, what is the range of the function?
```{julia}
#| hold: true
#| echo: false
choices = [
"The entire real line",
"The entire real line except for integer values",
"The integers"
]
answer = 3
radioq(choices, answ)
```
(This graphic uses the convention that a filled in point is present, but an open point is not, hence each bar represents some $[k, k+1)$.)
###### Question

View File

@@ -30,20 +30,57 @@ Why is this useful? When available, it can help us solve equations. If we can wr
Let's explore when we can "solve" for an inverse function.
Consider the graph of the function $f(x) = 2^x$:
Consider this graph of the function $f(x) = 2^x$
```{julia}
#| hold: true
f(x) = 2^x
plot(f, 0, 4, legend=false)
plot!([2,2,0], [0,f(2),f(2)])
#| echo: false
p = let
gr()
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
f(x) = 2^x
a, b = 0, 2
plot(; empty_style...)
xs = range(a, b, 200)
ys = f.(xs)
plot!( xs, ys; fn_style...)
plot!([a-1/4, b+.2], [0,0]; axis_style...)
plot!([0, 0], [-.1, f(2.1)]; axis_style...)
x = 1
y = (f(b)+f(a))/2
plot!([x,x,0],[0,f(x),f(x)]; line=(:black, 1, :dash), arrow=true, side=:head)
plot!([0,log2(y), log2(y)], [y,y,0]; line=(:black,1,:dash), arrow=true, side=:head)
annotate!([
(x, 0, text(L"c", 10, :top)),
(0,f(x), text(L"f(c)", 10, :right)),
(0, y, text(L"y=f(d)", 10, :right)),
(log2(y), 0, text(L"d", 10, :top))
])
end
plotly()
p
```
The graph of a function is a representation of points $(x,f(x))$, so to *find* $y = f(c)$ from the graph, we begin on the $x$ axis at $c$, move vertically to the graph (the point $(c, f(c))$), and then move horizontally to the $y$ axis, intersecting it at $y = f(c)$. The figure shows this for $c=2$, from which we can read that $f(c)$ is about $4$. This is how an $x$ is associated to a single $y$.
If we were to *reverse* the direction, starting at $y = f(c)$ on the $y$ axis and then moving horizontally to the graph, and then vertically to the $x$-axis we end up at a value $c$ with the correct $f(c)$. This allows solving for $x$ knowing $y$ in $y=f(x)$.
If we were to *reverse* the direction, starting at $y = f(d)$ on the $y$ axis and then moving horizontally to the graph, and then vertically to the $x$-axis we end up at a value $d$ with the correct $f(d)$. This allows solving for $x$ knowing $y$ in $y=f(x)$.
The operation described will form a function **if** the initial movement horizontally is guaranteed to find *no more than one* value on the graph. That is, to have an inverse function, there can not be two $x$ values corresponding to a given $y$ value. This observation is often visualized through the "horizontal line test" - the graph of a function with an inverse function can only intersect a horizontal line at most in one place.
@@ -309,6 +346,131 @@ What do we see? In blue, we can see the familiar square root graph along with a
This is reminiscent of the formula for the slope of a perpendicular line, $-1/m$, but quite different, as this formula implies the two lines have either both positive slopes or both negative slopes, unlike the relationship in slopes between a line and a perpendicular line.
::: {#fig-inverse-normal layout-ncol=1}
```{julia}
#| echo: false
# inverse function slope
gr()
p1 = let
f(x) = x^2
df(x) = 2x
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; aspect_ratio=:equal, empty_style...)
xs = range(0, 1.25, 100)
plot!(xs,f.(xs); fn_style...)
plot!(f.(xs), xs; fn2_style...)
plot!(identity, -1/4, 2; line=(:gray, 1, :dot))
#plot!([-.1, 1.35],[0,0]; axis_style...)
#plot!([0,0], [-0.1, f(1.3)]; axis_style...)
c = .4
m = df(c)
tl(x) = f(c) + df(c)*(x-c)
plot!(tl; line=(:black, 1, :dash))
d = c + .6
p1, p2, p3 = (c, tl(c)), (d, tl(c)), (d, tl(d))
q1, q2, q3 = (tl(c),c), (tl(c),d), (tl(d), d)
plot!([p1, p2, p3]; line=(:black, 1, :dot))
tl1(x) = c + (1/m)*(x - f(c))
plot!(tl1; line=(:red, 1, :dash))
plot!([q1, q2, q3]; line=(:red, 1, :dot))
annotate!([
((c+d)/2, f(c), text(L"\Delta x", 10, :top, :black)),
(d, (tl(c)+tl(d))/2, text(L"\Delta y", 10, :left, :black)),
(f(c), (c+d)/2, text(L"\Delta x", 10, :right, :red)),
((tl(c)+tl(d))/2, d, text(L"\Delta y", 10, :bottom, :red)),
(d, tl(d),
text(L"rise/run = $m = \Delta y / \Delta x$", 10, :top, :left,
rotation= rad2deg(atan(m)))),
(tl(d), d,
text(L"rise/run = $\Delta x / \Delta y = 1/m$", 10, :bottom, :left, rotation=rad2deg(atan(1/m)))),
(1.9, 1.9, text(L"y=x", 10, :top, rotation=45))
])
current()
end
# normal line
p2 = let
f(x) = 4 - (x-2)^2
df(x) = -2*(x-2)
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; aspect_ratio=:equal, empty_style...,
xlims=(1, 2.5), ylims=(3, 4.5))
xs = range(.99, 2.01, 100)
plot!(xs,f.(xs); fn_style...)
c = 1.5
tl(x) = f(c) + df(c)*(x-c)
nl(x) = f(c) - (x-c)/df(c)
xs = range(1, 2, 10)
plot!(xs, tl.(xs); fn2_style...)
xs = range(.9, 2, 10)
plot!(xs, nl.(xs); fn2_style...)
ylims!((f(.85), nl(.95)))
o = 1/3
plot!([c,c+o, c+o], [tl(c), tl(c), tl(c+o)]; mark_style...)
m = (tl(c+o) - tl(c))
plot!([c,c,c+m], [nl(c),nl(c + m),nl(c+m)]; mark_style...)
theta = rad2deg(atan(tl(c+o)-tl(c), o))
annotate!([
(c + o/2, f(c), text(L"1", :top, 10)),
(c + o, (f(c)+f(c+o))/2, text(L"m", :right, 10)),
(c, (nl(c) + nl(c+m))/2, text(L"-1", :right, 10)),
(c+m/2, nl(c+m), text(L"m", :top, 10)),
(c + o/2, tl(c+o), text(L"rise/run $=m/1$", 10, :top,
rotation=theta)),
(c + 1.1*o, nl(c+1.1*o), text(L"rise/run $=(-1)/m$", 10, :bottom,
rotation=theta-90))
])
current()
end
plot(p1, p2)
```
The inverse function has slope at a corresponding point that is the *reciprocal*; the "normal line" for a function at a point has slope that is the *negative reciprocal* of the "tangent line" at a point.
:::
```{julia}
#| echo: false
plotly()
nothing
```
The key here is that the shape of $f(x)$ near $x=c$ is somewhat related to the shape of $f^{-1}(x)$ at $f(c)$. In this case, if we use the tangent line as a fill in for how steep a function is, we see from the relationship that if $f(x)$ is "steep" at $x=c$, then $f^{-1}(x)$ will be "shallow" at $x=f(c)$.

View File

@@ -108,11 +108,6 @@ Plotting a function is then this simple: `plot(f, xmin, xmax)`.
:::{.callout-note}
## Note
The time to first plot can feel sluggish, but subsequent plots will be speedy. See the technical note at the end of this section for an explanation.
:::
Let's see some other graphs.
@@ -193,7 +188,70 @@ Some types we will encounter, such as the one for symbolic values or the special
:::
---
:::{.callout-note}
## Viewing window
The default style for `Plots.jl` is to use a frame style where the viewing window is emphasized. This is a rectangular region, $[x_0, x_1] \times [y_0, y_1]$, which is seen through the tick labeling, the bounding scales on the left and bottom, and emphasized through the grid.
This choices does *not* show the $x-y$ axes. As such, we might layer on the axes when these are of interest.
To emphasize concepts, we may stylize a function graph, rather than display the basic graphic. For example, in this graphic highlighting the amount the function goes up as it moves from $1$ to $x$:
```{julia}
gr()
#| echo: false
let
f(x) = x^2
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; empty_style..., aspect_ratio=:equal)
a, b = 0, 1.25
x = 1.15
plot!(f, a, b; fn_style...)
plot!([-.1, 1.5], [0,0]; axis_style...)
plot!([0,0], [-.1, f(1.35)]; axis_style...)
plot!([1,x,x], [f(1),f(1),f(x)]; line=(:black, 1))
plot!([1,1],[0,f(1)]; mark_style...)
plot!([x,x],[0,f(1)]; mark_style...)
plot!([0,1],[f(1),f(1)]; mark_style...)
plot!([0,x],[f(x),f(x)]; mark_style...)
annotate!([
(1, 0, text(L"1", 10, :top)),
(x, 0, text(L"x", 10, :top)),
(0, f(1), text(L"1", 10, :right)),
(0, f(x), text(L"x^2", 10, :right)),
(1, f(1), text(L"P", 10, :right, :bottom)),
(x, f(x), text(L"Q", 10, :right, :bottom)),
((1 + x)/2, f(1), text(L"\Delta x", 10, :top)),
(x, (f(1) + f(x))/2, text(L"\Delta y", 10, :left))
])
current()
end
```
```{julia}
#| echo: false
plotly()
nothing
```
:::
----
Making a graph with `Plots` is easy, but producing a graph that is informative can be a challenge, as the choice of a viewing window can make a big difference in what is seen. For example, trying to make a graph of $f(x) = \tan(x)$, as below, will result in a bit of a mess - the chosen viewing window crosses several places where the function blows up:
@@ -699,6 +757,40 @@ plot(f, g, 0, max((R-r)/r, r/(R-r))*2pi)
In the above, one can fix $R=1$. Then different values for `r` and `rho` will produce different graphs. These graphs will be periodic if $(R-r)/r$ is a rational. (Nothing about these equations requires $\rho < r$.)
## Points, lines, polygons
Two basic objects to graph are points and lines.
A point in two-dimensional space has two coordinates, often denoted by $(x,y)$. In `Julia`, the same notation produces a `tuple`. Using square brackets, as in `[x,y]`, produces a vector. Vectors are usually used, as we have seen there are algebraic operations defined for them. However, tuples have other advantages and are how `Plots` designates a point.
The plot command `plot(xs, ys)` plots the points $(x_1,y_1), \dots, (x_n, y_n)$ and then connects adjacent points with with lines. The command `scatter(xs, ys)` just plots the points.
However, the points might be more naturally specified as coordinate pairs. If tuples are used to pair them off, then `Plots` will plot a vector of tuples as a sequence of points:
```{julia}
pts = [(1, 0), (1/4, 1/4), (0, 1), (-1/4, 1/4), (-1, 0),
(-1/4, -1/4), (0, -1), (1/4, -1/4)]
scatter(pts; legend=false)
```
A line segment simply connects two points. While these can be specified as vectors of $x$ and $y$ values, again it may be more convenient to use coordinate pairs to specify the points. Continuing the above, we can connect adjacent points with line segments:
```{julia}
plot!(pts; line=(:gray, 0.5, :dash))
```
This uses the shorthand notation of `Plots` to specify `linecolor=:gray, linealpha=0.5, linestyle=:dash`. To plot just a line segment, just specifying two points suffices.
The four-pointed star is not closed off, as there isn't a value from the last point to the first point. A polygon closes itself off. The `Shape` function can take a vector of points or a pair of `xs` and `ys` to specify a polygon. When these are plotted, the arguments to `fill` describe the interior of the polygon, the arguments to `line` the boundary:
```{julia}
plot(Shape(pts); fill=(:gray, 0.25), line=(:black, 2), legend=false)
scatter!(pts)
```
## Questions