@bokeh/bokehjs
Version:
Interactive, novel data visualization
536 lines • 21 kB
JavaScript
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