502 lines
13 KiB
Julia
502 lines
13 KiB
Julia
|
|
|
|
typealias P2 FixedSizeArrays.Vec{2,Float64}
|
|
typealias P3 FixedSizeArrays.Vec{3,Float64}
|
|
|
|
nanpush!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); push!(a, b))
|
|
nanappend!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); append!(a, b))
|
|
nanpush!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); push!(a, b))
|
|
nanappend!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); append!(a, b))
|
|
compute_angle(v::P2) = (angle = atan2(v[2], v[1]); angle < 0 ? 2π - angle : angle)
|
|
|
|
# -------------------------------------------------------------
|
|
|
|
immutable Shape
|
|
x::Vector{Float64}
|
|
y::Vector{Float64}
|
|
function Shape(x::AVec, y::AVec)
|
|
if x[1] != x[end] || y[1] != y[end]
|
|
new(vcat(x, x[1]), vcat(y, y[1]))
|
|
else
|
|
new(x, y)
|
|
end
|
|
end
|
|
end
|
|
Shape(verts::AVec) = Shape(unzip(verts)...)
|
|
|
|
get_xs(shape::Shape) = shape.x
|
|
get_ys(shape::Shape) = shape.y
|
|
vertices(shape::Shape) = collect(zip(shape.x, shape.y))
|
|
|
|
|
|
function shape_coords(shape::Shape)
|
|
shape.x, shape.y
|
|
end
|
|
|
|
function shape_coords(shapes::AVec{Shape})
|
|
length(shapes) == 0 && return zeros(0), zeros(0)
|
|
xs = map(get_xs, shapes)
|
|
ys = map(get_ys, shapes)
|
|
x, y = map(copy, shape_coords(shapes[1]))
|
|
for shape in shapes[2:end]
|
|
nanappend!(x, shape.x)
|
|
nanappend!(y, shape.y)
|
|
end
|
|
x, y
|
|
end
|
|
|
|
"get an array of tuples of points on a circle with radius `r`"
|
|
function partialcircle(start_θ, end_θ, n = 20, r=1)
|
|
@compat(Tuple{Float64,Float64})[(r*cos(u),r*sin(u)) for u in linspace(start_θ, end_θ, n)]
|
|
end
|
|
|
|
"interleave 2 vectors into each other (like a zipper's teeth)"
|
|
function weave(x,y; ordering = Vector[x,y])
|
|
ret = eltype(x)[]
|
|
done = false
|
|
while !done
|
|
for o in ordering
|
|
try
|
|
push!(ret, shift!(o))
|
|
end
|
|
end
|
|
done = isempty(x) && isempty(y)
|
|
end
|
|
ret
|
|
end
|
|
|
|
|
|
"create a star by weaving together points from an outer and inner circle. `n` is the number of arms"
|
|
function makestar(n; offset = -0.5, radius = 1.0)
|
|
z1 = offset * π
|
|
z2 = z1 + π / (n)
|
|
outercircle = partialcircle(z1, z1 + 2π, n+1, radius)
|
|
innercircle = partialcircle(z2, z2 + 2π, n+1, 0.4radius)
|
|
Shape(weave(outercircle, innercircle)[1:end-2])
|
|
end
|
|
|
|
"create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle"
|
|
function makeshape(n; offset = -0.5, radius = 1.0)
|
|
z = offset * π
|
|
Shape(partialcircle(z, z + 2π, n+1, radius)[1:end-1])
|
|
end
|
|
|
|
|
|
function makecross(; offset = -0.5, radius = 1.0)
|
|
z2 = offset * π
|
|
z1 = z2 - π/8
|
|
outercircle = partialcircle(z1, z1 + 2π, 9, radius)
|
|
innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius)
|
|
Shape(weave(outercircle, innercircle,
|
|
ordering=Vector[outercircle,innercircle,outercircle])[1:end-2])
|
|
end
|
|
|
|
|
|
from_polar(angle, dist) = P2(dist*cos(angle), dist*sin(angle))
|
|
|
|
function makearrowhead(angle; h = 2.0, w = 0.4)
|
|
tip = from_polar(angle, h)
|
|
Shape(P2[(0,0), from_polar(angle - 0.5π, w) - tip,
|
|
from_polar(angle + 0.5π, w) - tip, (0,0)])
|
|
end
|
|
|
|
const _shapes = KW(
|
|
:ellipse => makeshape(20),
|
|
:rect => makeshape(4, offset=-0.25),
|
|
:diamond => makeshape(4),
|
|
:utriangle => makeshape(3),
|
|
:dtriangle => makeshape(3, offset=0.5),
|
|
:pentagon => makeshape(5),
|
|
:hexagon => makeshape(6),
|
|
:heptagon => makeshape(7),
|
|
:octagon => makeshape(8),
|
|
:cross => makecross(offset=-0.25),
|
|
:xcross => makecross(),
|
|
:vline => Shape([(0,1),(0,-1)]),
|
|
:hline => Shape([(1,0),(-1,0)]),
|
|
)
|
|
|
|
for n in [4,5,6,7,8]
|
|
_shapes[Symbol("star$n")] = makestar(n)
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
# uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon
|
|
function center(shape::Shape)
|
|
x, y = shape_coords(shape)
|
|
n = length(x)
|
|
A, Cx, Cy = 0.0, 0.0, 0.0
|
|
for i=1:n
|
|
ip1 = i==n ? 1 : i+1
|
|
A += x[i] * y[ip1] - x[ip1] * y[i]
|
|
end
|
|
A *= 0.5
|
|
for i=1:n
|
|
ip1 = i==n ? 1 : i+1
|
|
m = (x[i] * y[ip1] - x[ip1] * y[i])
|
|
Cx += (x[i] + x[ip1]) * m
|
|
Cy += (y[i] + y[ip1]) * m
|
|
end
|
|
Cx / 6A, Cy / 6A
|
|
end
|
|
|
|
function Base.scale!(shape::Shape, x::Real, y::Real = x, c = center(shape))
|
|
sx, sy = shape_coords(shape)
|
|
cx, cy = c
|
|
for i=1:length(sx)
|
|
sx[i] = (sx[i] - cx) * x + cx
|
|
sy[i] = (sy[i] - cy) * y + cy
|
|
end
|
|
shape
|
|
end
|
|
|
|
function Base.scale(shape::Shape, x::Real, y::Real = x, c = center(shape))
|
|
shapecopy = deepcopy(shape)
|
|
scale!(shape, x, y, c)
|
|
end
|
|
|
|
function translate!(shape::Shape, x::Real, y::Real = x)
|
|
sx, sy = shape_coords(shape)
|
|
for i=1:length(sx)
|
|
sx[i] += x
|
|
sy[i] += y
|
|
end
|
|
shape
|
|
end
|
|
|
|
function translate(shape::Shape, x::Real, y::Real = x)
|
|
shapecopy = deepcopy(shape)
|
|
translate!(shape, x, y)
|
|
end
|
|
|
|
function rotate_x(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real)
|
|
(x - centerx) * cos(Θ) - (y - centery) * sin(Θ) + centerx
|
|
end
|
|
|
|
function rotate_y(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real)
|
|
(y - centery) * cos(Θ) + (x - centerx) * sin(Θ) + centery
|
|
end
|
|
|
|
function rotate(x::Real, y::Real, θ::Real, c = center(shape))
|
|
cx, cy = c
|
|
rotate_x(x, y, Θ, cx, cy), rotate_y(x, y, Θ, cx, cy)
|
|
end
|
|
|
|
function rotate!(shape::Shape, Θ::Real, c = center(shape))
|
|
x, y = shape_coords(shape)
|
|
cx, cy = c
|
|
for i=1:length(x)
|
|
x[i] = rotate_x(x[i], y[i], Θ, cx, cy)
|
|
y[i] = rotate_y(x[i], y[i], Θ, cx, cy)
|
|
end
|
|
shape
|
|
end
|
|
|
|
function rotate(shape::Shape, Θ::Real, c = center(shape))
|
|
shapecopy = deepcopy(shape)
|
|
rotate!(shapecopy, Θ, c)
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
|
|
immutable Font
|
|
family::AbstractString
|
|
pointsize::Int
|
|
halign::Symbol
|
|
valign::Symbol
|
|
rotation::Float64
|
|
color::Colorant
|
|
end
|
|
|
|
"Create a Font from a list of unordered features"
|
|
function font(args...)
|
|
|
|
# defaults
|
|
family = "Helvetica"
|
|
pointsize = 14
|
|
halign = :hcenter
|
|
valign = :vcenter
|
|
rotation = 0.0
|
|
color = colorant"black"
|
|
|
|
for arg in args
|
|
T = typeof(arg)
|
|
|
|
if T == Font
|
|
family = arg.family
|
|
pointsize = arg.pointsize
|
|
halign = arg.halign
|
|
valign = arg.valign
|
|
rotation = arg.rotation
|
|
color = arg.color
|
|
elseif arg == :center
|
|
halign = :hcenter
|
|
valign = :vcenter
|
|
elseif arg in (:hcenter, :left, :right)
|
|
halign = arg
|
|
elseif arg in (:vcenter, :top, :bottom)
|
|
valign = arg
|
|
elseif T <: Colorant
|
|
color = arg
|
|
elseif T <: Symbol || T <: AbstractString
|
|
try
|
|
color = parse(Colorant, string(arg))
|
|
catch
|
|
family = string(arg)
|
|
end
|
|
elseif typeof(arg) <: Integer
|
|
pointsize = arg
|
|
elseif typeof(arg) <: Real
|
|
rotation = convert(Float64, arg)
|
|
else
|
|
warn("Unused font arg: $arg ($(typeof(arg)))")
|
|
end
|
|
end
|
|
|
|
Font(family, pointsize, halign, valign, rotation, color)
|
|
end
|
|
|
|
"Wrap a string with font info"
|
|
immutable PlotText
|
|
str::AbstractString
|
|
font::Font
|
|
end
|
|
PlotText(str) = PlotText(string(str), font())
|
|
|
|
text(t::PlotText) = t
|
|
text(str::AbstractString, f::Font) = PlotText(str, f)
|
|
function text(str, args...)
|
|
PlotText(string(str), font(args...))
|
|
end
|
|
|
|
|
|
annotations(::Void) = []
|
|
annotations(anns::AVec) = anns
|
|
annotations(anns) = Any[anns]
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
immutable Stroke
|
|
width
|
|
color
|
|
alpha
|
|
style
|
|
end
|
|
|
|
function stroke(args...; alpha = nothing)
|
|
width = nothing
|
|
color = nothing
|
|
style = nothing
|
|
|
|
for arg in args
|
|
T = typeof(arg)
|
|
|
|
# if arg in _allStyles
|
|
if allStyles(arg)
|
|
style = arg
|
|
elseif T <: Colorant
|
|
color = arg
|
|
elseif T <: Symbol || T <: AbstractString
|
|
try
|
|
color = parse(Colorant, string(arg))
|
|
end
|
|
elseif allAlphas(arg)
|
|
alpha = arg
|
|
elseif allReals(arg)
|
|
width = arg
|
|
else
|
|
warn("Unused stroke arg: $arg ($(typeof(arg)))")
|
|
end
|
|
end
|
|
|
|
Stroke(width, color, alpha, style)
|
|
end
|
|
|
|
|
|
immutable Brush
|
|
size # fillrange, markersize, or any other sizey attribute
|
|
color
|
|
alpha
|
|
end
|
|
|
|
function brush(args...; alpha = nothing)
|
|
size = nothing
|
|
color = nothing
|
|
|
|
for arg in args
|
|
T = typeof(arg)
|
|
|
|
if T <: Colorant
|
|
color = arg
|
|
elseif T <: Symbol || T <: AbstractString
|
|
try
|
|
color = parse(Colorant, string(arg))
|
|
end
|
|
elseif allAlphas(arg)
|
|
alpha = arg
|
|
elseif allReals(arg)
|
|
size = arg
|
|
else
|
|
warn("Unused brush arg: $arg ($(typeof(arg)))")
|
|
end
|
|
end
|
|
|
|
Brush(size, color, alpha)
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
"type which represents z-values for colors and sizes (and anything else that might come up)"
|
|
immutable ZValues
|
|
values::Vector{Float64}
|
|
zrange::Tuple{Float64,Float64}
|
|
end
|
|
|
|
function zvalues{T<:Real}(values::AVec{T}, zrange::Tuple{T,T} = (minimum(values), maximum(values)))
|
|
ZValues(collect(float(values)), map(Float64, zrange))
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
abstract AbstractSurface
|
|
|
|
"represents a contour or surface mesh"
|
|
immutable Surface{M<:AMat} <: AbstractSurface
|
|
surf::M
|
|
end
|
|
|
|
Surface(f::Function, x, y) = Surface(Float64[f(xi,yi) for yi in y, xi in x])
|
|
|
|
Base.Array(surf::Surface) = surf.surf
|
|
|
|
for f in (:length, :size)
|
|
@eval Base.$f(surf::Surface, args...) = $f(surf.surf, args...)
|
|
end
|
|
Base.copy(surf::Surface) = Surface(copy(surf.surf))
|
|
Base.eltype(surf::Surface) = eltype(surf.surf)
|
|
|
|
function expand_extrema!(a::Axis, surf::Surface)
|
|
ex = a[:extrema]
|
|
for vi in surf.surf
|
|
expand_extrema!(ex, vi)
|
|
end
|
|
ex
|
|
end
|
|
|
|
"For the case of representing a surface as a function of x/y... can possibly avoid allocations."
|
|
immutable SurfaceFunction <: AbstractSurface
|
|
f::Function
|
|
end
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
# style is :open or :closed (for now)
|
|
immutable Arrow
|
|
style::Symbol
|
|
headlength::Float64
|
|
headwidth::Float64
|
|
end
|
|
|
|
function arrow(args...)
|
|
style = :simple
|
|
headlength = 0.3
|
|
headwidth = 0.3
|
|
setlength = false
|
|
for arg in args
|
|
T = typeof(arg)
|
|
if T == Symbol
|
|
style = arg
|
|
elseif T <: Number
|
|
# first we apply to both, but if there's more, then only change width after the first number
|
|
headwidth = Float64(arg)
|
|
if !setlength
|
|
headlength = headwidth
|
|
end
|
|
setlength = true
|
|
elseif T <: Tuple && length(arg) == 2
|
|
headlength, headwidth = Float64(arg[1]), Float64(arg[2])
|
|
else
|
|
warn("Skipped arrow arg $arg")
|
|
end
|
|
end
|
|
Arrow(style, headlength, headwidth)
|
|
end
|
|
|
|
|
|
# allow for do-block notation which gets called on every valid start/end pair which
|
|
# we need to draw an arrow
|
|
function add_arrows(func::Function, x::AVec, y::AVec)
|
|
for i=2:length(x)
|
|
xyprev = (x[i-1], y[i-1])
|
|
xy = (x[i], y[i])
|
|
if ok(xyprev) && ok(xy)
|
|
if i==length(x) || !ok(x[i+1], y[i+1])
|
|
# add the arrow from xyprev to xy
|
|
func(xyprev, xy)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
|
|
type BezierCurve{T <: FixedSizeArrays.Vec}
|
|
control_points::Vector{T}
|
|
end
|
|
|
|
@compat function (bc::BezierCurve)(t::Real)
|
|
p = zero(P2)
|
|
n = length(bc.control_points)-1
|
|
for i in 0:n
|
|
p += bc.control_points[i+1] * binomial(n, i) * (1-t)^(n-i) * t^i
|
|
end
|
|
p
|
|
end
|
|
|
|
Base.mean(x::Real, y::Real) = 0.5*(x+y)
|
|
Base.mean{N,T<:Real}(ps::FixedSizeArrays.Vec{N,T}...) = sum(ps) / length(ps)
|
|
|
|
curve_points(curve::BezierCurve, n::Integer = 30; range = [0,1]) = map(curve, linspace(range..., n))
|
|
|
|
# build a BezierCurve which leaves point p vertically upwards and arrives point q vertically upwards.
|
|
# may create a loop if necessary. Assumes the view is [0,1]
|
|
function directed_curve(p::P2, q::P2; xview = 0:1, yview = 0:1)
|
|
mn = mean(p, q)
|
|
diff = q - p
|
|
|
|
minx, maxx = minimum(xview), maximum(xview)
|
|
miny, maxy = minimum(yview), maximum(yview)
|
|
diffpct = P2(diff[1] / (maxx - minx),
|
|
diff[2] / (maxy - miny))
|
|
|
|
# these points give the initial/final "rise"
|
|
# vertical_offset = P2(0, (maxy - miny) * max(0.03, min(abs(0.5diffpct[2]), 1.0)))
|
|
vertical_offset = P2(0, max(0.15, 0.5norm(diff)))
|
|
upper_control = p + vertical_offset
|
|
lower_control = q - vertical_offset
|
|
|
|
# try to figure out when to loop around vs just connecting straight
|
|
# TODO: choose loop direction based on sign of p[1]??
|
|
# x_close_together = abs(diffpct[1]) <= 0.05
|
|
p_is_higher = diff[2] <= 0
|
|
inside_control_points = if p_is_higher
|
|
# add curve points which will create a loop
|
|
sgn = mn[1] < 0.5 * (maxx + minx) ? -1 : 1
|
|
inside_offset = P2(0.3 * (maxx - minx), 0)
|
|
additional_offset = P2(sgn * diff[1], 0) # make it even loopier
|
|
[upper_control + sgn * (inside_offset + max(0, additional_offset)),
|
|
lower_control + sgn * (inside_offset + max(0, -additional_offset))]
|
|
else
|
|
[]
|
|
end
|
|
|
|
BezierCurve([p, upper_control, inside_control_points..., lower_control, q])
|
|
end
|