UNPKG

@quartic/bokehjs

Version:

Interactive, novel data visualization

212 lines (175 loc) 6.91 kB
import {XYGlyph, XYGlyphView} from "./xy_glyph" import * as hittest from "core/hittest" import * as p from "core/properties" export class CircleView extends XYGlyphView _map_data: () -> # NOTE: Order is important here: size is always present (at least # a default), but radius is only present if a user specifies it if @_radius? if @model.properties.radius.spec.units == "data" rd = @model.properties.radius_dimension.spec.value @sradius = @sdist(@renderer["#{rd}mapper"], @["_"+rd], @_radius) else @sradius = @_radius @max_size = 2 * @max_radius else @sradius = (s/2 for s in @_size) _mask_data: (all_indices) -> hr = @renderer.plot_view.frame.h_range vr = @renderer.plot_view.frame.v_range # check for radius first if @_radius? and @model.properties.radius.units == "data" sx0 = hr.start sx1 = hr.end [x0, x1] = @renderer.xmapper.v_map_from_target([sx0, sx1], true) x0 -= @max_radius x1 += @max_radius sy0 = vr.start sy1 = vr.end [y0, y1] = @renderer.ymapper.v_map_from_target([sy0, sy1], true) y0 -= @max_radius y1 += @max_radius else sx0 = hr.start - @max_size sx1 = hr.end + @max_size [x0, x1] = @renderer.xmapper.v_map_from_target([sx0, sx1], true) sy0 = vr.start - @max_size sy1 = vr.end + @max_size [y0, y1] = @renderer.ymapper.v_map_from_target([sy0, sy1], true) bbox = hittest.validate_bbox_coords([x0, x1], [y0, y1]) return @index.indices(bbox) _render: (ctx, indices, {sx, sy, sradius}) -> for i in indices if isNaN(sx[i]+sy[i]+sradius[i]) continue ctx.beginPath() ctx.arc(sx[i], sy[i], sradius[i], 0, 2*Math.PI, false) if @visuals.fill.doit @visuals.fill.set_vectorize(ctx, i) ctx.fill() if @visuals.line.doit @visuals.line.set_vectorize(ctx, i) ctx.stroke() _hit_point: (geometry) -> [vx, vy] = [geometry.vx, geometry.vy] x = @renderer.xmapper.map_from_target(vx, true) y = @renderer.ymapper.map_from_target(vy, true) # check radius first if @_radius? and @model.properties.radius.units == "data" x0 = x - @max_radius x1 = x + @max_radius y0 = y - @max_radius y1 = y + @max_radius else vx0 = vx - @max_size vx1 = vx + @max_size [x0, x1] = @renderer.xmapper.v_map_from_target([vx0, vx1], true) [x0, x1] = [Math.min(x0, x1), Math.max(x0, x1)] vy0 = vy - @max_size vy1 = vy + @max_size [y0, y1] = @renderer.ymapper.v_map_from_target([vy0, vy1], true) [y0, y1] = [Math.min(y0, y1), Math.max(y0, y1)] bbox = hittest.validate_bbox_coords([x0, x1], [y0, y1]) candidates = @index.indices(bbox) hits = [] if @_radius? and @model.properties.radius.units == "data" for i in candidates r2 = Math.pow(@sradius[i], 2) sx0 = @renderer.xmapper.map_to_target(x, true) sx1 = @renderer.xmapper.map_to_target(@_x[i], true) sy0 = @renderer.ymapper.map_to_target(y, true) sy1 = @renderer.ymapper.map_to_target(@_y[i], true) dist = Math.pow(sx0-sx1, 2) + Math.pow(sy0-sy1, 2) if dist <= r2 hits.push([i, dist]) else sx = @renderer.plot_view.canvas.vx_to_sx(vx) sy = @renderer.plot_view.canvas.vy_to_sy(vy) for i in candidates r2 = Math.pow(@sradius[i], 2) dist = Math.pow(@sx[i]-sx, 2) + Math.pow(@sy[i]-sy, 2) if dist <= r2 hits.push([i, dist]) return hittest.create_1d_hit_test_result(hits) _hit_span: (geometry) -> [vx, vy] = [geometry.vx, geometry.vy] {minX, minY, maxX, maxY} = this.bounds() result = hittest.create_hit_test_result() if geometry.direction == 'h' # use circle bounds instead of current pointer y coordinates y0 = minY y1 = maxY if @_radius? and @model.properties.radius.units == "data" vx0 = vx - @max_radius vx1 = vx + @max_radius [x0, x1] = @renderer.xmapper.v_map_from_target([vx0, vx1]) else ms = @max_size/2 vx0 = vx - ms vx1 = vx + ms [x0, x1] = @renderer.xmapper.v_map_from_target([vx0, vx1], true) else # use circle bounds instead of current pointer x coordinates x0 = minX x1 = maxX if @_radius? and @model.properties.radius.units == "data" vy0 = vy - @max_radius vy1 = vy + @max_radius [y0, y1] = @renderer.ymapper.v_map_from_target([vy0, vy1]) else ms = @max_size/2 vy0 = vy - ms vy1 = vy + ms [y0, y1] = @renderer.ymapper.v_map_from_target([vy0, vy1], true) bbox = hittest.validate_bbox_coords([x0, x1], [y0, y1]) hits = @index.indices(bbox) result['1d'].indices = hits return result _hit_rect: (geometry) -> [x0, x1] = @renderer.xmapper.v_map_from_target([geometry.vx0, geometry.vx1], true) [y0, y1] = @renderer.ymapper.v_map_from_target([geometry.vy0, geometry.vy1], true) bbox = hittest.validate_bbox_coords([x0, x1], [y0, y1]) result = hittest.create_hit_test_result() result['1d'].indices = @index.indices(bbox) return result _hit_poly: (geometry) -> [vx, vy] = [geometry.vx, geometry.vy] sx = @renderer.plot_view.canvas.v_vx_to_sx(vx) sy = @renderer.plot_view.canvas.v_vy_to_sy(vy) # TODO (bev) use spatial index to pare candidate list candidates = [0...@sx.length] hits = [] for i in [0...candidates.length] idx = candidates[i] if hittest.point_in_poly(@sx[i], @sy[i], sx, sy) hits.push(idx) result = hittest.create_hit_test_result() result['1d'].indices = hits return result # circle does not inherit from marker (since it also accepts radius) so we # must supply a draw_legend for it here draw_legend_for_index: (ctx, x0, x1, y0, y1, index) -> # using objects like this seems a little wonky, since the keys are coerced to # stings, but it works indices = [index] sx = { } sx[index] = (x0+x1)/2 sy = { } sy[index] = (y0+y1)/2 sradius = { } sradius[index] = Math.min(Math.abs(x1-x0), Math.abs(y1-y0))*0.2 data = {sx: sx, sy: sy, sradius: sradius} @_render(ctx, indices, data) export class Circle extends XYGlyph # XXX: Marker default_view: CircleView type: 'Circle' @mixins ['line', 'fill'] @define { angle: [ p.AngleSpec, 0 ] size: [ p.DistanceSpec, { units: "screen", value: 4 } ] radius: [ p.DistanceSpec, null ] radius_dimension: [ p.String, 'x' ] } initialize: (attrs, options) -> super(attrs, options) @properties.radius.optional = true