LogLevel(1001)
67 JavaScript based plotting libraries
table (generic function with 1 method)
This section uses this add-on package:
using PlotlyLight
To avoid a dependence on the CalculusWithJulia
package, we load two utility packages:
using PlotUtils
using SplitApplyCombine
Julia
has different interfaces to a few JavaScript plotting libraries, notably the vega and vega-lite through the VegaLite.jl package, and plotly 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 provides numerous examples which can easily be translated. The one-page-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.
67.1 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 for a scatter plot includes this JSON
structure:
= {
var trace1 : [1, 2, 3, 4],
x: [10, 15, 13, 17],
y: 'markers',
modetype: '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:
Config(x = [1,2,3,4],
= [10, 15, 13, 17],
y = "markers",
mode type = "scatter"
)
Config with 4 entries:
: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:
= Config()
cfg = "value"
cfg.key1.key2.key3 cfg
Config with 1 entry:
:key1 => Config(:key2=>Config(:key3=>"value"))
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.
67.2 Scatter plot
A basic scatter plot of points \((x,y)\) is created as follows:
= 1:5
xs = rand(5)
ys = Config(x = xs,
data = ys,
y type="scatter",
="markers"
mode
)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.
67.3 Line plot
A line plot is very similar, save for a different mode
specification:
= 1:5
xs = rand(5)
ys = Config(x = xs,
data = ys,
y type="scatter",
="lines"
mode
)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.
67.3.1 Nothing
The line graph plays connect-the-dots with the points specified by paired x
and y
values. Typically, when x
value is NaN
that “dot” (or point) is skipped. However, NaN
doesn’t pass through the JSON conversion – nothing
can be used.
= Config(
data =[0,1,nothing,3,4,5],
x= [0,1,2,3,4,5],
y type="scatter", mode="markers+lines")
Plot(data)
67.4 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:
= [Config(x = 1:5,
data = rand(5),
y = "scatter",
type = "markers",
mode = "scatter plot"),
name Config(x = 1:5,
= rand(5),
y = "scatter",
type = "lines",
mode = "line plot")
name
]Plot(data)
The name
argument adjusts the name in the legend referencing the plot. This is produced by default.
67.4.1 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.
= 0, 2pi
a, b
= PlotUtils.adapted_grid(sin, (a,b))
xs, ys = Plot(Config(x=xs, y=ys, name="sin"))
p
= PlotUtils.adapted_grid(cos, (a,b))
xs, ys push!(p.data, Config(x=xs, y=ys, name="cos"))
# to display the plot p
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.
= PlotUtils.adapted_grid(x -> x^5 - x - 1, (0, 2)) # answer is (0,2)
xs, ys = Plot([Config(x=xs, y=ys, name="Polynomial"),
p Config(x=xs, y=0 .* ys, name="x-axis", mode="lines", line=Config(width=5))]
)= filter(d -> !isnothing(get(d, :x, nothing)), p.data)
ds =reduce(min, [minimum(d.x) for d ∈ ds]; init=Inf)
a=reduce(max, [maximum(d.x) for d ∈ ds]; init=-Inf)
b (a, b)
(0.0, 2.0)
67.5 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.
67.6 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.
67.6.1 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:
= Config(x = 1:5,
data = rand(5),
y ="markers+text",
modetype="scatter",
="scatter plot",
name= ["marker $i" for i in 1:5],
text = "top center",
textposition = Config(size=12, color=:blue)
marker
)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:
struct rgb
r
g
bend
StructType(::Type{rgb}) = PlotlyLight.JSON3.StructTypes.StringType()
PlotlyLight.JSON3.StructTypes.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:
= [rgb(i,i,i) for i in range(10, 245, length=5)]
cols = [12, 16, 20, 24, 28]
sizes = Config(x = 1:5,
data = rand(5),
y ="markers+text",
modetype="scatter",
="scatter plot",
name= ["marker $i" for i in 1:5],
text = "top center",
textposition = Config(size=sizes, color=cols)
marker
)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.
= ["circle", "square", "diamond", "cross", "x", "triangle", "pentagon",
markers "hexagram", "star", "diamond", "hourglass", "bowtie", "asterisk",
"hash", "y", "line"]
= length(markers)
n = [Config(x=1:n, y=1:n, mode="markers",
data = Config(symbol=markers, size=10)),
marker Config(x=1:n, y=2 .+ (1:n), mode="markers",
= Config(symbol=markers .* "-open", size=10)),
marker Config(x=1:n, y=4 .+ (1:n), mode="markers",
= Config(symbol=markers .* "-open-dot", size=10))
marker
]Plot(data)
67.6.2 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:
= ["linear", "hv", "vh", "hvh", "vhv", "spline"]
shapes = [Config(x = 1:5, y = 5*(i-1) .+ [1,3,2,3,1], mode="lines+markers", type="scatter",
data =shape,
name=Config(shape=shape)
line∈ enumerate(shapes)]
) for (i, shape) Plot(data)
67.6.3 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
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
used to format the displayed text.
67.6.4 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.
= range(-1, 1, 100)
xs = [
data Config(
=xs, y=[1 - x^2/2 for x ∈ xs ],
x= "tozeroy",
fill = "rgba(255,0,0,0.25)", # to get transparency
fillcolor = Config(color=:red)
line
),Config(
=xs, y=cos.(xs),
x= "tonexty",
fill = "rgba(0,0,255,0.25)", # to get transparency
fillcolor = Config(color=:blue)
line
)
]Plot(data)
The toself
declaration is used below to fill in a polygon:
= Config(
data =[-1,1,1,-1,-1], y = [-1,1,-1,1,-1],
x="toself",
filltype="scatter")
Plot(data)
67.7 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.
= Config(x=1:5, y=rand(5), type="scatter", mode="markers", name="legend label")
data = Config(title = "Main chart title",
lyt = Config(title="x-axis label"),
xaxis = Config(title="y-axis label"),
yaxis =true
showlegend
)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:
= range(0, 2pi, length=100)
ts = Config(x = sin.(ts), y = cos.(ts), mode="lines", type="scatter")
data = Config(title = "A circle",
lyt = Config(title = "x"),
xaxis = Config(title = "y",
yaxis = "x")
scaleanchor
)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.
= Config(x = [0, 1], y = [0, 1], mode="markers", type="scatter")
data = Config(title = "Annotations",
layout = Config(title="x",
xaxis = (-0.5, 1.5)),
range = Config(title="y",
yaxis = (-0.5, 1.5)),
range = [
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 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.
= pi/6
alpha = pi/5
beta = cos(alpha)*cos(beta)
xₘ = sin(alpha+beta)
yₘ = 0.1
r₀
= [
data Config(
= [0,xₘ, xₘ, 0, 0],
x = [0, 0, yₘ, yₘ, 0],
y ="scatter", mode="line"
type
),Config(
= [0, xₘ],
x = [0, sin(alpha)*cos(beta)],
y = "tozeroy",
fill = "rgba(100, 100, 100, 0.5)"
fillcolor
),Config(
= [0, cos(alpha+beta), xₘ],
x = [0, yₘ, sin(alpha)*cos(beta)],
y = "tonexty",
fill = "rgba(200, 0, 100, 0.5)",
fillcolor
),Config(
= [0, cos(alpha+beta)],
x = [0, yₘ],
y = Config(width=5, color=:black)
line
)
]
= Config(
lyt =450,
height=false,
showlegend=Config(visible=false),
xaxis= Config(visible=false, scaleanchor="x"),
yaxis = [
annotations
Config(x = r₀*cos(alpha/2), y = r₀*sin(alpha/2),
="α", showarrow=false),
textConfig(x = r₀*cos(alpha+beta/2), y = r₀*sin(alpha+beta/2),
="β", showarrow=false),
textConfig(x = cos(alpha+beta) + r₀*cos(pi+(alpha+beta)/2),
= yₘ + r₀*sin(pi+(alpha+beta)/2),
y ="center", yanchor="center",
xanchor="α+β", showarrow=false),
textConfig(x = xₘ + r₀*cos(pi/2+alpha/2),
= sin(alpha)*cos(beta) + r₀ * sin(pi/2 + alpha/2),
y ="α", showarrow=false),
textConfig(x = 1/2 * cos(alpha+beta),
= 1/2 * sin(alpha+beta),
y = "1"),
text Config(x = xₘ/2*cos(alpha), y = xₘ/2*sin(alpha),
="center", yanchor="bottom",
xanchor= "cos(β)",
text =-rad2deg(alpha),
textangle=false),
showarrowConfig(x = xₘ + sin(beta)/2*cos(pi/2 + alpha),
= sin(alpha)*cos(beta) + sin(beta)/2*sin(pi/2 + alpha),
y ="center", yanchor="top",
xanchor= "sin(β)",
text = rad2deg(pi/2-alpha),
textangle =false),
showarrow
Config(x = xₘ/2,
= 0,
y ="center", yanchor="top",
xanchor= "cos(α)⋅cos(β)", showarrow=false),
text Config(x = 0,
= yₘ/2,
y ="right", yanchor="center",
xanchor= "sin(α+β)",
text =-90,
textangle=false),
showarrowConfig(x = cos(alpha+beta)/2,
= yₘ,
y ="center", yanchor="bottom",
xanchor= "cos(α+β)", showarrow=false),
text Config(x = cos(alpha+beta) + (xₘ - cos(alpha+beta))/2,
= yₘ,
y ="center", yanchor="bottom",
xanchor= "sin(α)⋅sin(β)", showarrow=false),
text Config(x = xₘ, y=sin(alpha)*cos(beta) + (yₘ - sin(alpha)*cos(beta))/2,
="left", yanchor="center",
xanchor= "cos(α)⋅sin(β)",
text =90,
textangle=false),
showarrowConfig(x = xₘ,
= sin(alpha)*cos(beta)/2,
y ="left", yanchor="center",
xanchor= "sin(α)⋅cos(β)",
text =90,
textangle=false)
showarrow
]
)
Plot(data, lyt)
67.8 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:
unzip(v) = SplitApplyCombine.invert(v)
unzip (generic function with 1 method)
Earlier, we plotted a two dimensional circle, here we plot the related helix.
helix(t) = [cos(t), sin(t), t]
= range(0, 4pi, length=200)
ts
= unzip(helix.(ts))
xs, ys, zs
= Config(x=xs, y=ys, z=zs,
data type = "scatter3d", # <<- note the 3d
= "lines",
mode =(width=2,
line=:red)
color
)
Plot(data)
The main difference is the chart type, as this is a \(3\)-dimensional plot, “scatter3d” is used.
67.8.1 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) 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:
helix(t) = [cos(t), sin(t), t]
helix′(t) = [-sin(t), cos(t), 1]
= range(0, 4pi, length=200)
ts = unzip(helix.(ts))
xs, ys, zs = Config(; NamedTuple(zip((:x,:y,:z), unzip(helix.(ts))))...,
helix_trace type = "scatter3d", # <<- note the 3d
= "lines",
mode =(width=2,
line=:red)
color
)
= pi/2:pi/2:7pi/2
tss = helix.(tss), helix′.(tss)
rs, r′s
= [
arrows Config(x = [x[1], x[1]+x′[1]],
= [x[2], x[2]+x′[2]],
y = [x[3], x[3]+x′[3]],
z ="lines", type="scatter3d")
mode∈ zip(rs, r′s)
for (x, x′)
]
= rs .+ r′s
tips = 0.1 * r′s
lengths
= Config(;
caps NamedTuple(zip([:x,:y,:z], unzip(tips)))...,
NamedTuple(zip([:u,:v,:w], unzip(lengths)))...,
type="cone", anchor="tail")
= vcat(helix_trace, arrows, caps)
data
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.
67.9 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:
f(x,y) = x^2 - 2y^2
= range(0,2,length=25)
xs = range(0,2, length=50)
ys = [[f(x,y) for x in xs] for y in ys]
zs
= Config(
data =xs, y=ys, z=zs,
xtype="contour"
)
Plot(data)
The same zs
data can be achieved by broadcasting and then collecting as follows:
f(x,y) = x^2 - 2y^2
= range(0,2,length=25)
xs = range(0,2, length=50)
ys = collect(eachrow(f.(xs', ys)))
zs
= Config(
data =xs, y=ys, z=zs,
xtype="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).
67.10 Surface plots
The chart type “surface” allows surfaces in \(3\) dimensions to be plotted.
67.10.1 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.
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)
= range(-3,3, length=50)
xs = range(-3,3, length=50)
ys = [[peaks(x,y) for x in xs] for y in ys]
zs
= Config(x=xs, y=ys, z=zs,
data type="surface")
Plot(data)
67.10.2 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
instructs the scene’s axes to be drawn in proportion with the axes’ ranges.
= 1, 5
r, R X(theta,phi) = [(r*cos(theta)+R)*cos(phi),
r*cos(theta)+R)*sin(phi),
(r*sin(theta)]
= range(0, 2pi, length=25)
us = range(0, pi, length=25)
vs
= [[X(u,v)[1] for u in us] for v in vs]
xs = [[X(u,v)[2] for u in us] for v in vs]
ys = [[X(u,v)[3] for u in us] for v in vs]
zs
= Config(
data = xs, y = ys, z = zs,
x type="surface",
="scatter3d"
mode
)
= Config(scene=Config(aspectmode="data"))
lyt
Plot(data, lyt)