@quartic/bokehjs
Version:
Interactive, novel data visualization
191 lines (154 loc) • 5.63 kB
text/coffeescript
import {DataRange} from "./data_range"
import {GlyphRenderer} from "../renderers/glyph_renderer"
import {logger} from "core/logging"
import * as p from "core/properties"
import * as bbox from "core/util/bbox"
export class DataRange1d extends DataRange
type: 'DataRange1d'
@define {
start: [ p.Number ]
end: [ p.Number ]
range_padding: [ p.Number, 0.1 ]
flipped: [ p.Bool, false ]
follow: [ p.String ] # TODO (bev)
follow_interval: [ p.Number ]
default_span: [ p.Number, 2 ]
bounds: [ p.Any ] # TODO (bev)
min_interval: [ p.Any ]
max_interval: [ p.Any ]
}
@internal {
mapper_hint: [ p.String, 'auto' ]
}
initialize: (attrs, options) ->
super(attrs, options)
@plot_bounds = {}
@have_updated_interactively = false
@_initial_start = @start
@_initial_end = @end
@_initial_range_padding = @range_padding
@_initial_follow = @follow
@_initial_follow_interval = @follow_interval
@_initial_default_span = @default_span
@getters {
min: () -> Math.min(@start, @end)
max: () -> Math.max(@start, @end)
}
computed_renderers: () ->
# TODO (bev) check that renderers actually configured with this range
names = @names
renderers = @renderers
if renderers.length == 0
for plot in @plots
all_renderers = plot.renderers
rs = (r for r in all_renderers when r instanceof GlyphRenderer)
renderers = renderers.concat(rs)
if names.length > 0
renderers = (r for r in renderers when names.indexOf(r.name) >= 0)
logger.debug("computed #{renderers.length} renderers for DataRange1d #{@id}")
for r in renderers
logger.trace(" - #{r.type} #{r.id}")
return renderers
_compute_plot_bounds: (renderers, bounds) ->
result = bbox.empty()
for r in renderers
if bounds[r.id]?
result = bbox.union(result, bounds[r.id])
return result
_compute_min_max: (plot_bounds, dimension) ->
overall = bbox.empty()
for k, v of plot_bounds
overall = bbox.union(overall, v)
if dimension == 0
[min, max] = [overall.minX, overall.maxX]
else
[min, max] = [overall.minY, overall.maxY]
return [min, max]
_compute_range: (min, max) ->
range_padding = @range_padding
if range_padding? and range_padding > 0
if @mapper_hint == "log"
if isNaN(min) or not isFinite(min) or min <= 0
if isNaN(max) or not isFinite(max) or max <= 0
min = 0.1
else
min = max / 100
logger.warn("could not determine minimum data value for log axis, DataRange1d using value #{min}")
if isNaN(max) or not isFinite(max) or max <= 0
if isNaN(min) or not isFinite(min) or min <= 0
max = 10
else
max = min * 100
logger.warn("could not determine maximum data value for log axis, DataRange1d using value #{max}")
log_min = Math.log(min) / Math.log(10)
log_max = Math.log(max) / Math.log(10)
if max == min
span = @default_span + 0.001
else
span = (log_max-log_min)*(1+range_padding)
center = (log_min+log_max) / 2.0
[start, end] = [Math.pow(10, center-span / 2.0), Math.pow(10, center+span / 2.0)]
else
if max == min
span = @default_span
else
span = (max-min)*(1+range_padding)
center = (max+min) / 2.0
[start, end] = [center-span / 2.0, center+span / 2.0]
else
[start, end] = [min, max]
follow_sign = +1
if @flipped
[start, end] = [end, start]
follow_sign = -1
follow_interval = @follow_interval
if follow_interval? and Math.abs(start-end) > follow_interval
if @follow == 'start'
end = start + follow_sign*follow_interval
else if @follow == 'end'
start = end - follow_sign*follow_interval
return [start, end]
update: (bounds, dimension, bounds_id) ->
if @have_updated_interactively
return
renderers = @computed_renderers()
# update the raw data bounds for all renderers we care about
@plot_bounds[bounds_id] = @_compute_plot_bounds(renderers, bounds)
# compute the min/mix for our specified dimension
[min, max] = @_compute_min_max(@plot_bounds, dimension)
# derive start, end from bounds and data range config
[start, end] = @_compute_range(min, max)
if @_initial_start?
if @mapper_hint == "log"
if @_initial_start > 0
start = @_initial_start
else
start = @_initial_start
if @_initial_end?
if @mapper_hint == "log"
if @_initial_end > 0
end = @_initial_end
else
end = @_initial_end
# only trigger updates when there are changes
[_start, _end] = [@start, @end]
if start != _start or end != _end
new_range = {}
if start != _start
new_range.start = start
if end != _end
new_range.end = end
@setv(new_range)
if @bounds == 'auto'
@setv({bounds: [start, end]}, {silent: true})
@trigger('change')
reset: () ->
@have_updated_interactively = false
# change events silenced as PlotCanvasView.update_dataranges triggers property callbacks
@setv({
range_padding: @_initial_range_padding
follow: @_initial_follow
follow_interval: @_initial_follow_interval
default_span: @_initial_default_span
}, {silent: true})
@trigger('change')