UNPKG

@quartic/bokehjs

Version:

Interactive, novel data visualization

125 lines (99 loc) 4.07 kB
import {Annotation, AnnotationView} from "./annotation" import {OpenHead} from "./arrow_head" import {ColumnDataSource} from "../sources/column_data_source" import * as p from "core/properties" import {atan2} from "core/util/math" export class ArrowView extends AnnotationView initialize: (options) -> super(options) if not @model.source? this.model.source = new ColumnDataSource() @canvas = @plot_model.canvas @xmapper = @plot_view.frame.x_mappers[@model.x_range_name] @ymapper = @plot_view.frame.y_mappers[@model.y_range_name] @set_data(@model.source) bind_bokeh_events: () -> @listenTo(@model, 'change', @plot_view.request_render) @listenTo(@model.source, 'change', () -> @set_data(@model.source) @plot_view.request_render()) set_data: (source) -> super(source) @visuals.warm_cache(source) _map_data: () -> if @model.start_units == 'data' start = @plot_view.map_to_screen(@_x_start, @_y_start, x_name=@model.x_range_name y_name=@model.y_range_name ) else start = [@canvas.v_vx_to_sx(@_x_start), @canvas.v_vy_to_sy(@_y_start)] if @model.end_units == 'data' end = @plot_view.map_to_screen(@_x_end, @_y_end, x_name=@model.x_range_name y_name=@model.y_range_name ) else end = [@canvas.v_vx_to_sx(@_x_end), @canvas.v_vy_to_sy(@_y_end)] return [start, end] render: () -> if not @model.visible return ctx = @plot_view.canvas_view.ctx ctx.save() # Order in this function is important. First we draw all the arrow heads. [@start, @end] = @_map_data() if @model.end? then @_arrow_head(ctx, "render", @model.end, @start, @end) if @model.start? then @_arrow_head(ctx, "render", @model.start, @end, @start) # Next we call .clip on all the arrow heads, inside an initial canvas sized # rect, to create an "inverted" clip region for the arrow heads ctx.beginPath(); ctx.rect(0, 0, @canvas.width, @canvas.height); if @model.end? then @_arrow_head(ctx, "clip", @model.end, @start, @end) if @model.start? then @_arrow_head(ctx, "clip", @model.start, @end, @start) ctx.closePath() ctx.clip(); # Finally we draw the arrow body, with the clipping regions set up. This prevents # "fat" arrows from overlapping the arrow head in a bad way. @_arrow_body(ctx) ctx.restore() _arrow_body: (ctx) -> if not @visuals.line.doit return for i in [0...@_x_start.length] @visuals.line.set_vectorize(ctx, i) ctx.beginPath() ctx.moveTo(@start[0][i], @start[1][i]) ctx.lineTo(@end[0][i], @end[1][i]) ctx.stroke() _arrow_head: (ctx, action, head, start, end) -> for i in [0...@_x_start.length] # arrow head runs orthogonal to arrow body angle = Math.PI/2 + atan2([start[0][i], start[1][i]], [end[0][i], end[1][i]]) ctx.save() ctx.translate(end[0][i], end[1][i]) ctx.rotate(angle) if action == "render" head.render(ctx) else if action == "clip" head.clip(ctx) ctx.restore() export class Arrow extends Annotation default_view: ArrowView type: 'Arrow' @mixins ['line'] @define { x_start: [ p.NumberSpec, ] y_start: [ p.NumberSpec, ] start_units: [ p.String, 'data' ] start: [ p.Instance, null ] x_end: [ p.NumberSpec, ] y_end: [ p.NumberSpec, ] end_units: [ p.String, 'data' ] end: [ p.Instance, new OpenHead({}) ] source: [ p.Instance ] x_range_name: [ p.String, 'default' ] y_range_name: [ p.String, 'default' ] }