series_annotations refactor and gr/pyplot fixes

This commit is contained in:
Thomas Breloff 2016-11-04 15:39:53 -04:00
parent af1896dc36
commit 4dfadeaf15
7 changed files with 239 additions and 99 deletions

View File

@ -194,7 +194,7 @@ const _series_defaults = KW(
:match_dimensions => false, # do rows match x (true) or y (false) for heatmap/image/spy? see issue 196
# this ONLY effects whether or not the z-matrix is transposed for a heatmap display!
:subplot => :auto, # which subplot(s) does this series belong to?
:series_annotations => [], # a list of annotations which apply to the coordinates of this series
:series_annotations => nothing, # a list of annotations which apply to the coordinates of this series
:primary => true, # when true, this "counts" as a series for color selection, etc. the main use is to allow
# one logical series to be broken up (path and markers, for example)
:hover => nothing, # text to display when hovering over the data points
@ -440,7 +440,7 @@ add_aliases(:match_dimensions, :transpose, :transpose_z)
add_aliases(:subplot, :sp, :subplt, :splt)
add_aliases(:projection, :proj)
add_aliases(:title_location, :title_loc, :titleloc, :title_position, :title_pos, :titlepos, :titleposition, :title_align, :title_alignment)
add_aliases(:series_annotations, :series_ann, :seriesann, :series_anns, :seriesanns, :series_annotation)
add_aliases(:series_annotations, :series_ann, :seriesann, :series_anns, :seriesanns, :series_annotation, :text, :txt, :texts, :txts)
add_aliases(:html_output_format, :format, :fmt, :html_format)
add_aliases(:orientation, :direction, :dir)
add_aliases(:inset_subplots, :inset, :floating)
@ -692,6 +692,11 @@ function preprocessArgs!(d::KW)
end
delete!(d, :fill)
# handle series annotations
if haskey(d, :series_annotations)
d[:series_annotations] = series_annotations(wraptuple(d[:series_annotations])...)
end
# convert into strokes and brushes
if haskey(d, :arrow)

View File

@ -56,8 +56,8 @@ function text_size(lablen::Int, sz::Number, rot::Number = 0)
# we need to compute the size of the ticks generically
# this means computing the bounding box and then getting the width/height
# note:
ptsz = 1.5sz * pt
width = 0.5lablen * ptsz
ptsz = sz * pt
width = 0.8lablen * ptsz
# now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles
height = abs(sind(rot)) * width + abs(cosd(rot)) * ptsz

View File

@ -269,12 +269,22 @@ end
# ---------------------------------------------------------
# draw ONE Shape
# function gr_draw_marker(xi, yi, msize, shape::Shape)
# sx, sy = shape_coords(shape)
# GR.selntran(0)
# xi, yi = GR.wctondc(xi, yi)
# GR.fillarea(xi + sx * 0.0015msize,
# yi + sy * 0.0015msize)
# GR.selntran(1)
# end
function gr_draw_marker(xi, yi, msize, shape::Shape)
sx, sy = shape_coords(shape)
# convert to ndc coords (percentages of window)
GR.selntran(0)
xi, yi = GR.wctondc(xi, yi)
GR.fillarea(xi + sx * 0.0015msize,
yi + sy * 0.0015msize)
ms_ndc_x, ms_ndc_y = gr_pixels_to_ndc(msize, msize)
GR.fillarea(xi .+ sx .* ms_ndc_x,
yi .+ sy .* ms_ndc_y)
GR.selntran(1)
end
@ -288,10 +298,11 @@ end
# draw the markers, one at a time
function gr_draw_markers(series::Series, x, y, msize, mz)
shape = series[:markershape]
if shape != :none
shapes = series[:markershape]
if shapes != :none
for i=1:length(x)
msi = cycle(msize, i)
shape = cycle(shapes, i)
cfunc = isa(shape, Shape) ? gr_set_fillcolor : gr_set_markercolor
cfuncind = isa(shape, Shape) ? GR.setfillcolorind : GR.setmarkercolorind
@ -371,6 +382,9 @@ end
# values are [xmin, xmax, ymin, ymax]. they range [0,1].
const viewport_plotarea = zeros(4)
# the size of the current plot in pixels
const gr_plot_size = zeros(2)
function gr_viewport_from_bbox(bb::BoundingBox, w, h, viewport_canvas)
viewport = zeros(4)
viewport[1] = viewport_canvas[2] * (left(bb) / w)
@ -425,6 +439,13 @@ gr_view_ycenter() = 0.5 * (viewport_plotarea[3] + viewport_plotarea[4])
gr_view_xdiff() = viewport_plotarea[2] - viewport_plotarea[1]
gr_view_ydiff() = viewport_plotarea[4] - viewport_plotarea[3]
function gr_pixels_to_ndc(x_pixels, y_pixels)
w,h = gr_plot_size
totx = w * gr_view_xdiff()
toty = h * gr_view_ydiff()
x_pixels / totx, y_pixels / toty
end
# --------------------------------------------------------------------------------------
@ -452,6 +473,7 @@ function gr_display(plt::Plot)
# compute the viewport_canvas, normalized to the larger dimension
viewport_canvas = Float64[0,1,0,1]
w, h = plt[:size]
gr_plot_size[:] = [w, h]
if w > h
ratio = float(h) / w
msize = display_width_ratio * w
@ -737,6 +759,10 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas)
x, y, z = series[:x], series[:y], series[:z]
frng = series[:fillrange]
# add custom frame shapes to markershape?
series_annotations_shapes!(series)
# -------------------------------------------------------
# recompute data
if typeof(z) <: Surface
# if st == :heatmap
@ -937,6 +963,53 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas)
GR.drawimage(xmin, xmax, ymax, ymin, w, h, rgba)
end
# if anns != nothing
# TODO handle series annotations
# TODO: this should be moved with SeriesAnnotations... iterate like:
# for (xi,yi,str,shape) in eachann(anns, sp) ... end
# or maybe
# anns.sp = sp
# for (xi,yi,str,shape) in anns ... end
# TODO: maybe scrap all of this and do some preprocessing to overwrite the marker shape with
# a vector of the computed shapes?? then marker_z, etc will still work
# @assert !is3d(sp)
# shapefillcolor = plot_color(ann.shapefill.color, ann.shapefill.alpha)
# shapestrokecolor = plot_color(ann.shapestroke.color, ann.shapestroke.alpha)
# for i=1:length(y)
# xi = cycle(x,i)
# yi = cycle(y,i)
# # @show anns.strs typeof(anns.strs)
# str = cycle(anns.strs,i)
# if !isnull(anns.baseshape)
# # get the width and height of the string (in mm)
# sw, sh = text_size(str, anns.font.pointsize)
#
# # how much to scale the base shape?
# xscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sw), sp, :x)
# yscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sh), sp, :y)
#
# # get the shape for this x/y/str
# shape = scale(get(anns.baseshape), xscale, yscale)
# translate!(shape, xi, yi)
#
# # draw the interior
# gr_set_fill(shapefillcolor)
# GR.fillarea(shape_coords(shape)...)
#
# # draw the shapes
# gr_set_line(anns.shapestroke.width, anns.shapestroke.style, shapestrokecolor)
# GR.polyline(shape_coords(shape)...)
# end
# this is all we need to add the series_annotations text
anns = series[:series_annotations]
for (xi,yi,str) in EachAnn(anns, x, y)
gr_set_font(anns.font)
gr_text(GR.wctondc(xi, yi)..., str)
end
# end
GR.restorestate()
end
@ -1021,55 +1094,14 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas)
end
end
for ann in sp[:annotations]
if isa(ann, SeriesAnnotations)
# TODO handle series annotations
# TODO: this should be moved with SeriesAnnotations... iterate like:
# for (xi,yi,str,shape) in eachann(anns, sp) ... end
# or maybe
# anns.sp = sp
# for (xi,yi,str,shape) in anns ... end
@assert !is3d(sp)
shapefillcolor = plot_color(ann.shapefill.color, ann.shapefill.alpha)
shapestrokecolor = plot_color(ann.shapestroke.color, ann.shapestroke.alpha)
for i=1:length(ann.y)
xi = cycle(ann.x,i)
yi = cycle(ann.y,i)
str = cycle(ann.strs,i)
if !isnull(ann.baseshape)
# get the width and height of the string (in mm)
sw, sh = text_size(str, ann.font.pointsize)
# how much to scale the base shape?
xscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sw), sp, :x)
yscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sh), sp, :y)
# get the shape for this x/y/str
shape = scale(get(ann.baseshape), xscale, yscale)
translate!(shape, xi, yi)
# draw the interior
gr_set_fill(shapefillcolor)
GR.fillarea(shape_coords(shape)...)
# draw the shapes
gr_set_line(ann.shapestroke.width, ann.shapestroke.style, shapestrokecolor)
GR.polyline(shape_coords(shape)...)
end
gr_set_font(ann.font)
gr_text(GR.wctondc(xi, yi)..., str)
end
x, y, val = ann
x, y = if is3d(sp)
# GR.wc3towc(x, y, z)
else
x, y, val = ann
x, y = if is3d(sp)
# GR.wc3towc(x, y, z)
else
GR.wctondc(x, y)
end
gr_set_font(val.font)
gr_text(x, y, val.str)
GR.wctondc(x, y)
end
gr_set_font(val.font)
gr_text(x, y, val.str)
end
GR.restorestate()
end

View File

@ -445,6 +445,9 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series)
error("Only numbers and vectors are supported with levels keyword")
end
# add custom frame shapes to markershape?
series_annotations_shapes!(series, :xy)
# for each plotting command, optionally build and add a series handle to the list
# line plot
@ -560,16 +563,46 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series)
else
xyargs
end
handle = ax[:scatter](xyargs...;
label = series[:label],
zorder = series[:series_plotindex] + 0.5,
marker = py_marker(series[:markershape]),
s = py_dpi_scale(plt, series[:markersize] .^ 2),
edgecolors = py_markerstrokecolor(series),
linewidths = py_dpi_scale(plt, series[:markerstrokewidth]),
extrakw...
)
push!(handles, handle)
if isa(series[:markershape], AbstractVector{Shape})
# this section will create one scatter per data point to accomodate the
# vector of shapes
handle = []
x,y = xyargs
shapes = series[:markershape]
msc = py_markerstrokecolor(series)
lw = py_dpi_scale(plt, series[:markerstrokewidth])
for i=1:length(y)
extrakw[:c] = if series[:marker_z] == nothing
py_color_fix(py_color(cycle(series[:markercolor],i)), x)
else
extrakw[:c]
end
push!(handle, ax[:scatter](cycle(x,i), cycle(y,i);
label = series[:label],
zorder = series[:series_plotindex] + 0.5,
marker = py_marker(cycle(shapes,i)),
s = py_dpi_scale(plt, cycle(series[:markersize],i) .^ 2),
edgecolors = msc,
linewidths = lw,
extrakw...
))
end
push!(handles, handle)
else
# do a normal scatter plot
handle = ax[:scatter](xyargs...;
label = series[:label],
zorder = series[:series_plotindex] + 0.5,
marker = py_marker(series[:markershape]),
s = py_dpi_scale(plt, series[:markersize] .^ 2),
edgecolors = py_markerstrokecolor(series),
linewidths = py_dpi_scale(plt, series[:markerstrokewidth]),
extrakw...
)
push!(handles, handle)
end
end
if st == :hexbin
@ -849,6 +882,12 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series)
)
push!(handles, handle)
end
# this is all we need to add the series_annotations text
anns = series[:series_annotations]
for (xi,yi,str) in EachAnn(anns, x, y)
py_add_annotations(sp, xi, yi, PlotText(str, anns.font))
end
end
# --------------------------------------------------------------------------

View File

@ -23,6 +23,7 @@ immutable Shape
# end
end
Shape(verts::AVec) = Shape(unzip(verts)...)
Shape(s::Shape) = deepcopy(s)
get_xs(shape::Shape) = shape.x
get_ys(shape::Shape) = shape.y
@ -143,6 +144,8 @@ for n in [4,5,6,7,8]
_shapes[Symbol("star$n")] = makestar(n)
end
Shape(k::Symbol) = deepcopy(_shapes[k])
# -----------------------------------------------------------------------
@ -307,7 +310,7 @@ function text(str, args...)
PlotText(string(str), font(args...))
end
Base.length(t::PlotText) = length(t.str)
# -----------------------------------------------------------------------
@ -386,44 +389,104 @@ end
type SeriesAnnotations
strs::AbstractVector # the labels/names
font::Font
baseshape::Nullable{Shape}
shapefill::Brush
shapestroke::Stroke
x
y
baseshape::Nullable
# shapefill::Brush
# shapestroke::Stroke
# x
# y
end
function series_annotations(strs::AbstractVector, args...)
fnt = font()
shp = Nullable{Shape}()
br = brush(:steelblue)
stk = stroke()
α = nothing
shp = Nullable{Any}()
scalefactor = 1
# br = brush(:steelblue)
# stk = stroke()
# α = nothing
for arg in args
if isa(arg, Shape)
shp = Nullable{Shape}(arg)
elseif isa(arg, Brush)
brush = arg
elseif isa(arg, Stroke)
stk = arg
if isa(arg, Shape) || (isa(arg, AbstractVector) && eltype(arg) == Shape)
shp = Nullable(arg)
# elseif isa(arg, Brush)
# brush = arg
# elseif isa(arg, Stroke)
# stk = arg
elseif isa(arg, Font)
fnt = arg
elseif isa(arg, Symbol) && haskey(_shapes, arg)
shp = _shapes[arg]
elseif allAlphas(arg)
α = arg
# elseif allAlphas(arg)
# α = arg
elseif isa(arg, Number)
scalefactor = arg
else
warn("Unused SeriesAnnotations arg: $arg ($(typeof(arg)))")
end
end
if α != nothing
br.alpha = α
stk.alpha = α
if scalefactor != 1
for s in get(shp)
scale!(s, scalefactor, scalefactor, (0,0))
end
end
# if α != nothing
# br.alpha = α
# stk.alpha = α
# end
# note: x/y coords are added later
SeriesAnnotations(strs, fnt, shp, br, stk, nothing, nothing)
SeriesAnnotations(strs, fnt, shp)
end
series_annotations(anns::SeriesAnnotations) = anns
series_annotations(::Void) = nothing
function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels)
anns = series[:series_annotations]
if anns != nothing && !isnull(anns.baseshape)
# x = series[:x]
# y = series[:y]
# we should use baseshape to overwrite the markershape attribute
# with a list of custom shapes for each
msize = Float64[]
shapes = Shape[begin
# xi = cycle(x,i)
# yi = cycle(y,i)
str = cycle(anns.strs,i)
# get the width and height of the string (in mm)
sw, sh = text_size(str, anns.font.pointsize)
# how much to scale the base shape?
# note: it's a rough assumption that the shape fills the unit box [-1,-1,1,1],
# so we scale the length-2 shape by 1/2 the total length
# if scaletype == :pixels
scalar = (backend() == PyPlotBackend() ? 1.7 : 1.0)
xscale = 0.5to_pixels(sw) * scalar
yscale = 0.55to_pixels(sh) * scalar
# else
# sp = series[:subplot]
# xscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sw), sp, :x)
# yscale = 0.5 * resolve_mixed(MixedMeasures(0, 0, sh), sp, :y)
# end
maxscale = max(xscale, yscale)
push!(msize, maxscale)
# get the shape for this x/y/str
# @show get(anns.baseshape) xscale,yscale
baseshape = cycle(get(anns.baseshape),i)
shape = scale(baseshape, xscale/maxscale, yscale/maxscale, (0,0))
# @show shape
end for i=1:length(anns.strs)]
series[:markershape] = shapes
series[:markersize] = msize #1 # the scaling is handled in the shapes
end
return
end
type EachAnn
anns
x
y
end
Base.start(ea::EachAnn) = 1
Base.done(ea::EachAnn, i) = ea.anns == nothing || isempty(ea.anns.strs) || i > length(ea.y)
Base.next(ea::EachAnn, i) = ((cycle(ea.x,i), cycle(ea.y,i), cycle(ea.anns.strs,i)), i+1)
annotations(::Void) = []
annotations(anns::AVec) = anns

View File

@ -111,6 +111,7 @@ function resolve_mixed(mix::MixedMeasures, sp::Subplot, letter::Symbol)
if mix.len != 0mm
f = (letter == :x ? width : height)
totlen = f(plotarea(sp))
@show totlen
pct += mix.len / totlen
end
if pct != 0

View File

@ -333,17 +333,17 @@ function _prepare_annotations(sp::Subplot, d::KW)
# strip out series annotations (those which are based on series x/y coords)
# and add them to the subplot attr
sp_anns = annotations(sp[:annotations])
series_anns = annotations(pop!(d, :series_annotations, []))
if isa(series_anns, SeriesAnnotations)
series_anns.x = d[:x]
series_anns.y = d[:y]
elseif length(series_anns) > 0
x, y = d[:x], d[:y]
nx, ny, na = map(length, (x,y,series_anns))
n = max(nx, ny, na)
series_anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(series_anns[mod1(i,na)])) for i=1:n]
end
sp.attr[:annotations] = vcat(sp_anns, series_anns)
# series_anns = annotations(pop!(d, :series_annotations, []))
# if isa(series_anns, SeriesAnnotations)
# series_anns.x = d[:x]
# series_anns.y = d[:y]
# elseif length(series_anns) > 0
# x, y = d[:x], d[:y]
# nx, ny, na = map(length, (x,y,series_anns))
# n = max(nx, ny, na)
# series_anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(series_anns[mod1(i,na)])) for i=1:n]
# end
# sp.attr[:annotations] = vcat(sp_anns, series_anns)
end
function _expand_subplot_extrema(sp::Subplot, d::KW, st::Symbol)