UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

660 lines 25.9 kB
import { GuideRenderer, GuideRendererView } from "../renderers/guide_renderer"; import { Ticker } from "../tickers/ticker"; import { TickFormatter } from "../formatters/tick_formatter"; import { LabelingPolicy, AllLabels } from "../policies/labeling"; import { AxisClick } from "../../core/bokeh_events"; import * as mixins from "../../core/property_mixins"; import { Align, Face, LabelOrientation, AxisLabelStandoffMode } from "../../core/enums"; import { Indices } from "../../core/types"; import { SidePanel, SideLayout } from "../../core/layout/side_panel"; import { sum, repeat } from "../../core/util/array"; import { dict } from "../../core/util/object"; import { isNumber } from "../../core/util/types"; import { GraphicsBoxes, TextBox } from "../../core/graphics"; import { FactorRange } from "../ranges/factor_range"; import { BaseText } from "../text/base_text"; import { build_view } from "../../core/build_views"; import { unreachable } from "../../core/util/assert"; import { isString } from "../../core/util/types"; import { BBox } from "../../core/util/bbox"; import { parse_delimited_string } from "../text/utils"; import { Str, Float, Ref, Or, Dict, Mapping } from "../../core/kinds"; export const LabelOverrides = Or(Dict(Or(Str, Ref(BaseText))), Mapping(Or(Str, Float), Or(Str, Ref(BaseText)))); const { abs } = Math; export class AxisView extends GuideRendererView { static __name__ = "AxisView"; get panel() { return this._panel; } set panel(panel) { this._panel = new SidePanel(panel.side, this.model.face); } /*private*/ _axis_label_view = null; /*private*/ _major_label_views = new Map(); get bbox() { if (this.model.fixed_location != null) { return new BBox(); } else if (this.layout != null) { return this.layout.bbox; } else if (this.is_renderable) { const { extents } = this; const depth = Math.round(extents.tick + extents.tick_label + extents.axis_label); let { sx0, sy0, sx1, sy1 } = this.rule_scoords; const { dimension, face } = this; if (dimension == 0) { if (face == "front") { sy0 -= depth; } else { sy1 += depth; } } else { if (face == "front") { sx0 -= depth; } else { sx1 += depth; } } return BBox.from_lrtb({ left: sx0, top: sy0, right: sx1, bottom: sy1 }); } else { return new BBox(); } } children_views() { const this_axis_label_view = this._axis_label_view != null ? [this._axis_label_view] : []; return [...super.children_views(), ...this_axis_label_view, ...this._major_label_views.values()]; } async lazy_initialize() { await super.lazy_initialize(); await this._init_axis_label(); await this._init_major_labels(); } async _init_axis_label() { const { axis_label } = this.model; if (axis_label != null) { const _axis_label = isString(axis_label) ? parse_delimited_string(axis_label) : axis_label; this._axis_label_view = await build_view(_axis_label, { parent: this }); } else { this._axis_label_view = null; } } async _init_major_labels() { for (const [label, label_text] of dict(this.model.major_label_overrides)) { const _label_text = isString(label_text) ? parse_delimited_string(label_text) : label_text; this._major_label_views.set(label, await build_view(_label_text, { parent: this })); } } update_layout() { this.layout = new SideLayout(this.panel, () => this.get_size(), true); this.layout.on_resize(() => { this._coordinates = undefined; }); } get_size() { const { visible, fixed_location } = this.model; if (visible && fixed_location == null && this.is_renderable) { const { extents } = this; const height = Math.round(extents.tick + extents.tick_label + extents.axis_label); return { width: 0, height }; } else { return { width: 0, height: 0 }; } } get is_renderable() { const [range, cross_range] = this.ranges; return super.is_renderable && range.is_valid && cross_range.is_valid && range.span > 0 && cross_range.span > 0; } interactive_hit(sx, sy) { return this.bbox.contains(sx, sy); } on_hit(sx, sy) { const value = this._hit_value(sx, sy); if (value != null) { this.model.trigger_event(new AxisClick(this.model, value)); return true; } return false; } _paint(ctx) { const { tick_coords, extents } = this; this._draw_background(ctx, extents); this._draw_rule(ctx, extents); this._draw_major_ticks(ctx, extents, tick_coords); this._draw_minor_ticks(ctx, extents, tick_coords); this._draw_major_labels(ctx, extents, tick_coords); this._draw_axis_label(ctx, extents, tick_coords); } connect_signals() { super.connect_signals(); const { axis_label, major_label_overrides } = this.model.properties; this.on_change(axis_label, async () => { this._axis_label_view?.remove(); await this._init_axis_label(); }); this.on_change(major_label_overrides, async () => { for (const label_view of this._major_label_views.values()) { label_view.remove(); } await this._init_major_labels(); }); this.connect(this.model.change, () => this.plot_view.request_layout()); this.connect(this.model.ticker.change, () => this.plot_view.request_layout()); } get needs_clip() { return this.model.fixed_location != null; } // drawing sub functions ----------------------------------------------------- _draw_background(ctx, _extents) { if (!this.visuals.background_fill.doit && !this.visuals.background_hatch.doit) { return; } ctx.beginPath(); const { x, y, width, height } = this.bbox; ctx.rect(x, y, width, height); this.visuals.background_fill.apply(ctx); this.visuals.background_hatch.apply(ctx); } _draw_rule(ctx, _extents) { if (!this.visuals.axis_line.doit) { return; } const { sx0, sy0, sx1, sy1 } = this.rule_scoords; ctx.beginPath(); ctx.moveTo(sx0, sy0); ctx.lineTo(sx1, sy1); this.visuals.axis_line.apply(ctx); } _draw_major_ticks(ctx, _extents, tick_coords) { const tin = this.model.major_tick_in; const tout = this.model.major_tick_out; const visuals = this.visuals.major_tick_line; this._draw_ticks(ctx, tick_coords.major, tin, tout, visuals); } _draw_minor_ticks(ctx, _extents, tick_coords) { const tin = this.model.minor_tick_in; const tout = this.model.minor_tick_out; const visuals = this.visuals.minor_tick_line; this._draw_ticks(ctx, tick_coords.minor, tin, tout, visuals); } _draw_major_labels(ctx, extents, tick_coords) { const coords = tick_coords.major; const labels = this.compute_labels(coords[this.dimension]); const orient = this.model.major_label_orientation; const standoff = extents.tick + this.model.major_label_standoff; const visuals = this.visuals.major_label_text; this._draw_oriented_labels(ctx, labels, coords, orient, standoff, visuals); } _axis_label_extent() { if (this._axis_label_view == null) { return 0; } const axis_label_graphics = this._axis_label_view.graphics(); const padding = 3; const orient = this.model.axis_label_orientation; axis_label_graphics.visuals = this.visuals.axis_label_text.values(); axis_label_graphics.angle = this.panel.get_label_angle_heuristic(orient); axis_label_graphics.base_font_size = this.plot_view.base_font_size; const size = axis_label_graphics.size(); const extent = this.dimension == 0 ? size.height : size.width; const standoff_offset = (() => { switch (this.model.axis_label_standoff_mode) { case "tick_labels": return 0; case "axis": return sum(this._tick_label_extents()) + this._tick_extent(); } })(); const standoff = this.model.axis_label_standoff - standoff_offset; return extent > 0 ? standoff + extent + padding : 0; } _draw_axis_label(ctx, extents, _tick_coords) { if (this._axis_label_view == null) { return; } const [sx, sy /* TODO, x_anchor, y_anchor*/] = (() => { const { bbox } = this; const { side, face } = this.panel; const [range] = this.ranges; const { axis_label_align } = this.model; switch (side) { case "above": case "below": { const [sx, x_anchor] = (() => { switch (axis_label_align) { case "start": return !range.is_reversed ? [bbox.left, "left"] : [bbox.right, "right"]; case "center": return [bbox.hcenter, "center"]; case "end": return !range.is_reversed ? [bbox.right, "right"] : [bbox.left, "left"]; } })(); const [sy, y_anchor] = face == "front" ? [bbox.bottom, "bottom"] : [bbox.top, "top"]; return [sx, sy, x_anchor, y_anchor]; } case "left": case "right": { const [sy, y_anchor] = (() => { switch (axis_label_align) { case "start": return !range.is_reversed ? [bbox.bottom, "bottom"] : [bbox.top, "top"]; case "center": return [bbox.vcenter, "center"]; case "end": return !range.is_reversed ? [bbox.top, "top"] : [bbox.bottom, "bottom"]; } })(); const [sx, x_anchor] = face == "front" ? [bbox.right, "right"] : [bbox.left, "left"]; return [sx, sy, x_anchor, y_anchor]; } } })(); const [nx, ny] = this.normals; const orient = this.model.axis_label_orientation; const standoff_mode = this.model.axis_label_standoff_mode; const standoff_offset = (() => { switch (standoff_mode) { case "tick_labels": return extents.tick + extents.tick_label; case "axis": return 0; } })(); const standoff = this.model.axis_label_standoff + standoff_offset; const { vertical_align, align } = this.panel.get_label_text_heuristics(orient); const position = { sx: sx + nx * standoff, sy: sy + ny * standoff, x_anchor: align, y_anchor: vertical_align, }; const axis_label_graphics = this._axis_label_view.graphics(); axis_label_graphics.visuals = this.visuals.axis_label_text.values(); axis_label_graphics.angle = this.panel.get_label_angle_heuristic(orient); axis_label_graphics.base_font_size = this.plot_view.base_font_size; axis_label_graphics.position = position; axis_label_graphics.align = align; axis_label_graphics.paint(ctx); } _draw_ticks(ctx, coords, tin, tout, visuals) { if (!visuals.doit) { return; } const [sxs, sys] = this.scoords(coords); const [nx, ny] = this.normals; const [nxin, nyin] = [nx * -tin, ny * -tin]; const [nxout, nyout] = [nx * tout, ny * tout]; visuals.set_value(ctx); ctx.beginPath(); for (let i = 0; i < sxs.length; i++) { const sx0 = Math.round(sxs[i] + nxout); const sy0 = Math.round(sys[i] + nyout); const sx1 = Math.round(sxs[i] + nxin); const sy1 = Math.round(sys[i] + nyin); ctx.moveTo(sx0, sy0); ctx.lineTo(sx1, sy1); } ctx.stroke(); } _draw_oriented_labels(ctx, labels, coords, orient, standoff, visuals) { if (!visuals.doit || labels.length == 0) { return; } const [sxs, sys] = this.scoords(coords); const [nx, ny] = this.normals; const nxd = nx * standoff; const nyd = ny * standoff; const { vertical_align, align } = this.panel.get_label_text_heuristics(orient); const angle = this.panel.get_label_angle_heuristic(orient); labels.visuals = visuals.values(); labels.angle = angle; labels.base_font_size = this.plot_view.base_font_size; for (let i = 0; i < labels.length; i++) { const label = labels.items[i]; label.position = { sx: sxs[i] + nxd, sy: sys[i] + nyd, x_anchor: align, y_anchor: vertical_align, }; if (label instanceof TextBox) { label.align = align; } } const n = labels.length; const indices = Indices.all_set(n); const { items } = labels; const bboxes = items.map((l) => l.bbox()); const dist = (() => { const [range] = this.ranges; if (!range.is_reversed) { return this.dimension == 0 ? (i, j) => bboxes[j].left - bboxes[i].right : (i, j) => bboxes[i].top - bboxes[j].bottom; } else { return this.dimension == 0 ? (i, j) => bboxes[i].left - bboxes[j].right : (i, j) => bboxes[j].top - bboxes[i].bottom; } })(); const { major_label_policy } = this.model; const selected = major_label_policy.filter(indices, bboxes, dist); const ids = [...selected]; if (ids.length != 0) { const cbox = this.canvas.bbox; const correct_x = (k) => { const bbox = bboxes[k]; if (bbox.left < 0) { const offset = -bbox.left; const { position } = items[k]; items[k].position = { ...position, sx: position.sx + offset }; } else if (bbox.right > cbox.width) { const offset = bbox.right - cbox.width; const { position } = items[k]; items[k].position = { ...position, sx: position.sx - offset }; } }; const correct_y = (k) => { const bbox = bboxes[k]; if (bbox.top < 0) { const offset = -bbox.top; const { position } = items[k]; items[k].position = { ...position, sy: position.sy + offset }; } else if (bbox.bottom > cbox.height) { const offset = bbox.bottom - cbox.height; const { position } = items[k]; items[k].position = { ...position, sy: position.sy - offset }; } }; const i = ids[0]; const j = ids[ids.length - 1]; if (this.dimension == 0) { correct_x(i); correct_x(j); } else { correct_y(i); correct_y(j); } } for (const i of selected) { const label = items[i]; label.paint(ctx); } } // extents sub functions ----------------------------------------------------- /*protected*/ _tick_extent() { const { major, minor } = this.tick_coords; const i = this.dimension; return Math.max(major[i].length == 0 ? 0 : this.model.major_tick_out, minor[i].length == 0 ? 0 : this.model.minor_tick_out); } _tick_label_extents() { const coords = this.tick_coords.major; const labels = this.compute_labels(coords[this.dimension]); const orient = this.model.major_label_orientation; const standoff = this.model.major_label_standoff; const visuals = this.visuals.major_label_text; return [this._oriented_labels_extent(labels, orient, standoff, visuals)]; } get extents() { const tick_labels = this._tick_label_extents(); return { tick: this._tick_extent(), tick_labels, tick_label: sum(tick_labels), axis_label: this._axis_label_extent(), }; } _oriented_labels_extent(labels, orient, standoff, visuals) { if (labels.length == 0 || !visuals.doit) { return 0; } const angle = this.panel.get_label_angle_heuristic(orient); labels.visuals = visuals.values(); labels.angle = angle; labels.base_font_size = this.plot_view.base_font_size; const size = labels.max_size(); const extent = this.dimension == 0 ? size.height : size.width; const padding = 3; return extent > 0 ? standoff + extent + padding : 0; } // {{{ TODO: state get normals() { return this.panel.normals; } get dimension() { return this.panel.dimension; } compute_labels(ticks) { const labels = this.model.formatter.format_graphics(ticks, this); const { _major_label_views } = this; const visited = new Set(); for (let i = 0; i < ticks.length; i++) { const override = _major_label_views.get(ticks[i]); if (override != null) { visited.add(override); labels[i] = override.graphics(); } } // XXX: make sure unused overrides don't prevent document idle for (const label_view of this._major_label_views.values()) { if (!visited.has(label_view)) { label_view._has_finished = true; } } return new GraphicsBoxes(labels); } scoords(coords) { /** * Compute screen coordinates with respect to the bbox. */ const [x, y] = coords; const [sxs, sys] = this.coordinates.map_to_screen(x, y); if (this.model.fixed_location != null) { return [[...sxs], [...sys]]; } else { const { bbox } = this; const { face } = this.panel; if (this.panel.is_vertical) { const sx = face == "front" ? bbox.right : bbox.left; return [repeat(sx, sxs.length), [...sys]]; } else { const sy = face == "front" ? bbox.bottom : bbox.top; return [[...sxs], repeat(sy, sys.length)]; } } } get ranges() { const i = this.dimension; const j = 1 - i; const { ranges } = this.coordinates; return [ranges[i], ranges[j]]; } get computed_bounds() { const [range] = this.ranges; const user_bounds = this.model.bounds; const range_bounds = [range.min, range.max]; if (user_bounds == "auto") { return [range.min, range.max]; } else { let start; let end; const [user_start, user_end] = user_bounds; const [range_start, range_end] = range_bounds; const { min, max } = Math; if (abs(user_start - user_end) > abs(range_start - range_end)) { start = max(min(user_start, user_end), range_start); end = min(max(user_start, user_end), range_end); } else { start = min(user_start, user_end); end = max(user_start, user_end); } return [start, end]; } } get rule_coords() { const i = this.dimension; const j = 1 - i; const [range] = this.ranges; const [start, end] = this.computed_bounds; const xs = new Array(2); const ys = new Array(2); const coords = [xs, ys]; coords[i][0] = Math.max(start, range.min); coords[i][1] = Math.min(end, range.max); if (coords[i][0] > coords[i][1]) { coords[i][0] = coords[i][1] = NaN; } coords[j][0] = this.loc; coords[j][1] = this.loc; return coords; } get rule_scoords() { const [[sx0, sx1], [sy0, sy1]] = this.scoords(this.rule_coords); return { sx0: Math.round(sx0), sy0: Math.round(sy0), sx1: Math.round(sx1), sy1: Math.round(sy1), }; } get tick_coords() { const i = this.dimension; const j = 1 - i; const [range] = this.ranges; const [start, end] = this.computed_bounds; const ticks = this.model.ticker.get_ticks(start, end, range, this.loc); const majors = ticks.major; const minors = ticks.minor; const xs = []; const ys = []; const coords = [xs, ys]; const minor_xs = []; const minor_ys = []; const minor_coords = [minor_xs, minor_ys]; const [range_min, range_max] = [range.min, range.max]; for (let ii = 0; ii < majors.length; ii++) { if (majors[ii] < range_min || majors[ii] > range_max) { continue; } coords[i].push(majors[ii]); coords[j].push(this.loc); } for (let ii = 0; ii < minors.length; ii++) { if (minors[ii] < range_min || minors[ii] > range_max) { continue; } minor_coords[i].push(minors[ii]); minor_coords[j].push(this.loc); } return { major: coords, minor: minor_coords, }; } get loc() { const { fixed_location } = this.model; if (fixed_location != null) { if (isNumber(fixed_location)) { return fixed_location; } const [, cross_range] = this.ranges; if (cross_range instanceof FactorRange) { return cross_range.synthetic(fixed_location); } unreachable(); } const [, cross_range] = this.ranges; switch (this.panel.side) { case "left": case "below": return cross_range.start; case "right": case "above": return cross_range.end; } } get face() { return this.panel.face; } // }}} remove() { this._axis_label_view?.remove(); for (const label_view of this._major_label_views.values()) { label_view.remove(); } super.remove(); } has_finished() { if (!super.has_finished()) { return false; } if (this._axis_label_view != null) { if (!this._axis_label_view.has_finished()) { return false; } } for (const label_view of this._major_label_views.values()) { if (!label_view.has_finished()) { return false; } } return true; } } export class Axis extends GuideRenderer { static __name__ = "Axis"; constructor(attrs) { super(attrs); } static { this.mixins([ ["axis_", mixins.Line], ["major_tick_", mixins.Line], ["minor_tick_", mixins.Line], ["major_label_", mixins.Text], ["axis_label_", mixins.Text], ["background_", mixins.Fill], ["background_", mixins.Hatch], ]); this.define(({ Any, Int, Float, Str, Ref, Tuple, Or, Nullable, Auto, Enum }) => ({ dimension: [Or(Enum(0, 1), Auto), "auto"], face: [Or(Face, Auto), "auto"], bounds: [Or(Tuple(Float, Float), Auto), "auto"], ticker: [Ref(Ticker)], formatter: [Ref(TickFormatter)], axis_label: [Nullable(Or(Str, Ref(BaseText))), null], axis_label_standoff: [Int, 5], axis_label_standoff_mode: [AxisLabelStandoffMode, "tick_labels"], axis_label_orientation: [Or(LabelOrientation, Float), "parallel"], axis_label_align: [Align, "center"], major_label_standoff: [Int, 5], major_label_orientation: [Or(LabelOrientation, Float), "horizontal"], major_label_overrides: [LabelOverrides, new Map()], major_label_policy: [Ref(LabelingPolicy), () => new AllLabels()], major_tick_in: [Float, 2], major_tick_out: [Float, 6], minor_tick_in: [Float, 0], minor_tick_out: [Float, 4], fixed_location: [Nullable(Or(Float, Any)), null], })); this.override({ axis_line_color: "black", major_tick_line_color: "black", minor_tick_line_color: "black", major_label_text_font_size: "11px", major_label_text_align: "center", // XXX: remove major_label_text_baseline: "alphabetic", // XXX: remove axis_label_text_font_size: "13px", axis_label_text_font_style: "italic", background_fill_color: null, }); } } //# sourceMappingURL=axis.js.map