UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

536 lines 21 kB
import { Annotation, AnnotationView } from "./annotation"; import { Dimensional, MetricLength } from "./dimensional"; import { Range } from "../ranges/range"; import { Range1d } from "../ranges/range1d"; import { Align, Orientation, Location, HAlign, VAlign } from "../../core/enums"; import * as enums from "../../core/enums"; import * as mixins from "../../core/property_mixins"; import { TextBox } from "../../core/graphics"; import { SideLayout, SidePanel } from "../../core/layout/side_panel"; import { BBox } from "../../core/util/bbox"; import { TextLayout, FixedLayout } from "../../core/layout"; import { Grid } from "../../core/layout/grid"; import { LinearAxis } from "../axes/linear_axis"; import { Ticker } from "../tickers/ticker"; import { FixedTicker } from "../tickers/fixed_ticker"; import { LinearScale } from "../scales/linear_scale"; import { CategoricalScale } from "../scales/categorical_scale"; import { CoordinateTransform } from "../coordinates/coordinate_mapping"; import { build_view } from "../../core/build_views"; import { clamp } from "../../core/util/math"; import { assert } from "../../core/util/assert"; import { enumerate } from "../../core/util/iterator"; import { isString } from "../../core/util/types"; import { process_placeholders, sprintf } from "../../core/util/templating"; import { Enum, Or, Tuple, Float } from "../../core/kinds"; import { AutoAnchor } from "../common/kinds"; import * as resolve from "../common/resolve"; import { Factor } from "../ranges/factor_range"; const { round } = Math; const Position = Or(enums.Anchor, Tuple(Or(Float, Factor, HAlign), Or(Float, Factor, VAlign))); const PositionUnits = Enum("data", "screen", "view", "percent"); const LengthUnits = Enum("screen", "data", "percent"); const LengthSizing = Enum("adaptive", "exact"); export class ScaleBarView extends AnnotationView { static __name__ = "ScaleBarView"; _bbox = new BBox(); get bbox() { return this._bbox; } label_layout; title_layout; axis_layout; box_layout; axis; axis_view; axis_scale; cross_scale; range; _get_size() { const { width, height } = this.bbox; const { margin } = this.model; return { width: width + 2 * margin, height: height + 2 * margin, }; } initialize() { super.initialize(); const { ticker } = this.model; this.axis = new LinearAxis({ ticker, ...mixins.attrs_of(this.model, "bar_", mixins.Line, "axis_"), }); this.range = (() => { const { range, orientation } = this.model; if (range == "auto") { const { frame } = this.parent; switch (orientation) { case "horizontal": return frame.x_range; case "vertical": return frame.y_range; } } else { return range; } })(); } async lazy_initialize() { await super.lazy_initialize(); const coordinates = (() => { const axis_source = new Range1d(); const axis_target = new Range1d(); const cross_source = new Range1d(); const cross_target = new Range1d(); this.axis_scale = new LinearScale({ source_range: axis_source, target_range: axis_target }); this.cross_scale = new LinearScale({ source_range: cross_source, target_range: cross_target }); if (this.model.orientation == "horizontal") { return new CoordinateTransform(this.axis_scale, this.cross_scale); } else { return new CoordinateTransform(this.cross_scale, this.axis_scale); } })(); this.axis_view = await build_view(this.axis, { parent: this.plot_view }); this.axis_view.coordinates = coordinates; this.axis_view.panel = new SidePanel(this.model.orientation == "horizontal" ? "below" : "right"); this.axis_view.update_layout(); } remove() { this.axis_view.remove(); super.remove(); } connect_signals() { super.connect_signals(); this.connect(this.model.change, () => { this.request_paint(); }); this.connect(this.range.change, () => { this.request_paint(); }); } update_layout() { this.update_geometry(); const { panel } = this; if (panel != null) { this.layout = new SideLayout(panel, () => this.get_size()); } else { this.layout = undefined; } } update_geometry() { super.update_geometry(); } get horizontal() { return this.model.orientation == "horizontal"; } text_layout(args) { const { text, location, align, visuals } = args; const { orientation } = this.model; const text_box = new TextBox({ text }); const text_panel = new SidePanel(location); text_box.visuals = visuals.values(); const text_orientation = (() => { switch (location) { case "above": case "below": return "horizontal"; default: return orientation; } })(); text_box.angle = text_panel.get_label_angle_heuristic(text_orientation); text_box.base_font_size = this.plot_view.base_font_size; text_box.position = { sx: 0, sy: 0, x_anchor: "left", y_anchor: "top", }; text_box.align = "auto"; const text_layout = new TextLayout(text_box); text_layout.absolute = true; const horizontal = orientation == "horizontal"; const halign = horizontal ? align : undefined; const valign = !horizontal ? align : undefined; text_layout.set_sizing({ width_policy: "min", height_policy: "min", visible: text != "" && visuals.doit, halign, valign, }); return text_layout; } compute_geometry() { super.compute_geometry(); const { orientation, length_sizing, padding, margin } = this.model; const { border_line, bar_line } = this.visuals; const bar_width = bar_line.line_width.get_value(); const border_width = border_line.line_width.get_value(); const { frame } = this.parent; const frame_span = orientation == "horizontal" ? frame.bbox.width : frame.bbox.height; const bar_length_percent = (() => { const { bar_length, bar_length_units } = this.model; switch (bar_length_units) { case "screen": { if (0.0 <= bar_length && bar_length <= 1.0) { return bar_length; } else { return clamp(bar_length / frame_span, 0.0, 1.0); } } case "data": { const scale = orientation == "horizontal" ? this.coordinates.x_scale : this.coordinates.y_scale; assert(scale instanceof LinearScale || scale instanceof CategoricalScale); const [sv0, sv1] = scale.r_compute(0, bar_length); const sdist = Math.abs(sv1 - sv0); return sdist / frame_span; } case "percent": { return clamp(bar_length, 0.0, 1.0); } } })(); const { new_value, new_unit, scale_factor, exact } = (() => { const { unit, dimensional } = this.model; const value = this.range.span * bar_length_percent; return dimensional.compute(value, unit, length_sizing == "exact"); })(); const init_bar_length_px = frame_span * bar_length_percent; const bar_length_px = round(init_bar_length_px * scale_factor); const label_text = (() => { const { label } = this.model; return process_placeholders(label, (_, name, format) => { switch (name) { case "value": { if (exact) { if (format != null) { return sprintf(format, new_value); } else { return new_value.toFixed(2); } } else { return `${new_value}`; } } case "unit": { switch (format ?? "short") { case "short": return new_unit; } } default: { return null; } } }); })(); this.label_layout = this.text_layout({ text: label_text, location: this.model.label_location, align: this.model.label_align, visuals: this.visuals.label_text, }); this.title_layout = this.text_layout({ text: this.model.title, location: this.model.title_location, align: this.model.title_align, visuals: this.visuals.title_text, }); const bar_size = (() => { if (orientation == "horizontal") { return { width: bar_length_px, height: bar_width }; } else { return { width: bar_width, height: bar_length_px }; } })(); const axis_layout = this.axis_view.layout; assert(axis_layout != null); this.axis_layout = axis_layout; axis_layout.absolute = true; if (orientation == "horizontal") { axis_layout.set_sizing({ width_policy: "fixed", width: bar_size.width, height_policy: "min", valign: "center", }); } else { axis_layout.set_sizing({ width_policy: "min", height_policy: "fixed", height: bar_size.height, halign: "center", }); } this.box_layout = (() => { const panels = { above: [], below: [], left: [], right: [], }; function spacer(location, spacing) { const layout = new FixedLayout(); layout.absolute = true; layout.set_sizing((() => { if (location == "left" || location == "right") { return { width_policy: "fixed", width: spacing }; } else { return { height_policy: "fixed", height: spacing }; } })()); return layout; } function insert(layout, location, spacing) { if (layout.visible) { panels[location].push(spacer(location, spacing), layout); } } insert(this.label_layout, this.model.label_location, this.model.label_standoff); insert(this.title_layout, this.model.title_location, this.model.title_standoff); const row = panels.above.length; const col = panels.left.length; const items = [ { layout: axis_layout, row, col }, ]; for (const [layout, i] of enumerate(panels.above)) { items.push({ layout, row: row - i - 1, col }); } for (const [layout, i] of enumerate(panels.below)) { items.push({ layout, row: row + i + 1, col }); } for (const [layout, i] of enumerate(panels.left)) { items.push({ layout, row, col: col - i - 1 }); } for (const [layout, i] of enumerate(panels.right)) { items.push({ layout, row, col: col + i + 1 }); } return new Grid(items); })(); const { box_layout } = this; box_layout.absolute = true; box_layout.position = { left: padding, top: padding }; box_layout.set_sizing(); box_layout.compute(); const [axis_range, cross_range] = (() => { const { x_range, y_range } = this.axis_view.bbox; if (orientation == "horizontal") { return [x_range, y_range]; } else { return [y_range, x_range]; } })(); this.axis_scale.source_range.end = new_value; this.axis_scale.target_range.setv(axis_range); this.cross_scale.source_range.end = 1.0; this.cross_scale.target_range.setv(cross_range); const position = (() => { const { location: position } = this.model; if (isString(position)) { const normalized = (() => { switch (position) { case "top": return "top_center"; case "bottom": return "bottom_center"; case "left": return "center_left"; case "center": return "center_center"; case "right": return "center_right"; default: return position; } })(); const [v_loc, h_loc] = normalized.split("_"); return { x: h_loc, y: v_loc }; } else { const [x_loc, y_loc] = position; return { x: x_loc, y: y_loc }; } })(); const { x, y } = (() => { const { bbox } = this.layout ?? this.plot_view.frame; const inset = bbox.shrink_by(margin); const x_pos = (() => { const { x } = position; switch (x) { case "left": return inset.left; case "center": return inset.x_center; case "right": return inset.right; } const x_mapper = (() => { switch (this.model.x_units) { case "data": return this.coordinates.x_scale; case "screen": return bbox.x_screen; case "view": return bbox.x_view; case "percent": return bbox.x_percent; } })(); return x_mapper.compute( // @ts-ignore(TS2345): Argument of type 'number | ...' is not assignable to parameter of type 'number'. x); })(); const y_pos = (() => { const { y } = position; switch (y) { case "top": return inset.top; case "center": return inset.y_center; case "bottom": return inset.bottom; } const y_mapper = (() => { switch (this.model.y_units) { case "data": return this.coordinates.y_scale; case "screen": return bbox.y_screen; case "view": return bbox.y_view; case "percent": return bbox.y_percent; } })(); return y_mapper.compute( // @ts-ignore(TS2345): Argument of type 'number | ...' is not assignable to parameter of type 'number'. y); })(); return { x: x_pos, y: y_pos }; })(); const anchor = (() => { const anchor = resolve.anchor(this.model.anchor); const x_anchor = (() => { if (anchor.x == "auto") { switch (position.x) { case "left": return 0.0; case "center": return 0.5; case "right": return 1.0; default: return 0.5; } } else { return anchor.x; } })(); const y_anchor = (() => { if (anchor.y == "auto") { switch (position.y) { case "top": return 0.0; case "center": return 0.5; case "bottom": return 1.0; default: return 0.5; } } else { return anchor.y; } })(); return { x: x_anchor, y: y_anchor }; })(); const width = border_width + padding + box_layout.bbox.width + padding + border_width; const height = border_width + padding + box_layout.bbox.height + padding + border_width; const sx = x - anchor.x * width; const sy = y - anchor.y * height; this._bbox = new BBox({ left: sx, top: sy, width, height }); } _draw_box(ctx) { const { width, height } = this.bbox; ctx.beginPath(); ctx.rect(0, 0, width, height); this.visuals.background_fill.apply(ctx); this.visuals.background_hatch.apply(ctx); this.visuals.border_line.apply(ctx); } _draw_axis(ctx) { this.axis_view.paint(ctx); } _draw_text(ctx, layout, location) { const { bbox } = layout; const [x_offset, y_offset] = (() => { const { orientation } = this.model; const horizontal = orientation == "horizontal"; switch (location) { case "left": return horizontal ? [0, 0] : [0, bbox.height]; case "right": return horizontal ? [0, 0] : [bbox.width, 0]; case "above": return [0, 0]; case "below": return [0, 0]; } })(); const { left, top } = bbox.translate(x_offset, y_offset); ctx.translate(left, top); layout.text.paint(ctx); ctx.translate(-left, -top); } _draw_label(ctx) { this._draw_text(ctx, this.label_layout, this.model.label_location); } _draw_title(ctx) { this._draw_text(ctx, this.title_layout, this.model.title_location); } _paint(ctx) { const { left, top } = this.bbox; ctx.translate(left, top); if (this.box_layout.visible) { this._draw_box(ctx); } if (this.axis_layout.visible) { this._draw_axis(ctx); } if (this.label_layout.visible) { this._draw_label(ctx); } if (this.title_layout.visible) { this._draw_title(ctx); } ctx.translate(-left, -top); } } export class ScaleBar extends Annotation { static __name__ = "ScaleBar"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = ScaleBarView; this.mixins([ ["background_", mixins.Fill], ["background_", mixins.Hatch], ["bar_", mixins.Line], ["border_", mixins.Line], ["label_", mixins.Text], ["title_", mixins.Text], ]); this.define(({ NonNegative, Float, Str, Ref, Or, Auto }) => ({ anchor: [AutoAnchor, "auto"], bar_length: [NonNegative(Float), 0.2], bar_length_units: [LengthUnits, "screen"], dimensional: [Ref(Dimensional), () => new MetricLength()], label: [Str, "@{value} @{unit}"], label_align: [Align, "center"], label_location: [Location, "below"], label_standoff: [Float, 5], length_sizing: [LengthSizing, "adaptive"], location: [Position, "top_right"], margin: [Float, 10], orientation: [Orientation, "horizontal"], padding: [Float, 10], range: [Or(Ref(Range), Auto), "auto"], ticker: [Ref(Ticker), () => new FixedTicker({ ticks: [] })], title: [Str, ""], title_align: [Align, "center"], title_location: [Location, "above"], title_standoff: [Float, 5], unit: [Str, "m"], x_units: [PositionUnits, "data"], y_units: [PositionUnits, "data"], })); this.override({ background_fill_alpha: 0.95, background_fill_color: "#ffffff", bar_line_width: 2, border_line_alpha: 0.5, border_line_color: "#e5e5e5", border_line_width: 1, label_text_baseline: "middle", label_text_font_size: "13px", title_text_font_size: "13px", title_text_font_style: "italic", }); } } //# sourceMappingURL=scale_bar.js.map