diff --git a/src/args.jl b/src/args.jl index 490eaddc..0c248321 100644 --- a/src/args.jl +++ b/src/args.jl @@ -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) diff --git a/src/backends.jl b/src/backends.jl index 4873b171..16fcad2c 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -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 diff --git a/src/backends/gr.jl b/src/backends/gr.jl index ecf0845e..d182c821 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -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 diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index abac4017..2a342c90 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -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 # -------------------------------------------------------------------------- diff --git a/src/components.jl b/src/components.jl index e3b8f2a1..47d1c75c 100644 --- a/src/components.jl +++ b/src/components.jl @@ -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 diff --git a/src/layouts.jl b/src/layouts.jl index aa003a90..a5814ba1 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -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 diff --git a/src/pipeline.jl b/src/pipeline.jl index 9593b55c..7f3cc00e 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -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)