@bokeh/bokehjs
Version:
Interactive, novel data visualization
666 lines • 25.8 kB
JavaScript
import { isVectorized } from "../core/vectorization";
import { VectorSpec, UnitsSpec } from "../core/properties";
import { extend } from "../core/class";
import { is_equal, Comparator } from "../core/util/eq";
import { includes, uniq, zip } from "../core/util/array";
import { clone, keys, entries, is_empty, dict } from "../core/util/object";
import { isNumber, isString, isArray, isArrayOf, isPlainObject } from "../core/util/types";
import { enumerate } from "../core/util/iterator";
import * as nd from "../core/util/ndarray";
import { Axis, CategoricalAxis, CategoricalScale, ColumnDataSource, ColumnarDataSource, ContinuousTicker, CoordinateMapping, DataRange1d, DatetimeAxis, FactorRange, GlyphRenderer, Grid, LinearAxis, LinearScale, LogAxis, LogScale, MercatorAxis, Range, Range1d, Tool, ToolProxy, } from "./models";
import { Legend } from "../models/annotations/legend";
import { LegendItem } from "../models/annotations/legend_item";
import { Figure as BaseFigure } from "../models/plots/figure";
import { GestureTool } from "../models/tools/gestures/gesture_tool";
import { GlyphAPI } from "./glyph_api";
const _default_tools = ["pan", "wheel_zoom", "box_zoom", "save", "reset", "help"];
// export type ExtMarkerType = MarkerType | "*" | "+" | "o" | "ox" | "o+"
const _default_color = "#1f77b4";
const _default_alpha = 1.0;
class ModelProxy {
models;
static __name__ = "ModelProxy";
constructor(models) {
this.models = models;
const mapping = new Map();
for (const model of models) {
for (const prop of model) {
const { attr } = prop;
if (!mapping.has(attr)) {
mapping.set(attr, []);
}
mapping.get(attr).push(prop);
}
}
for (const [name, props] of mapping) {
Object.defineProperty(this, name, {
get() {
throw new Error("only setting values is supported");
},
set(value) {
for (const prop of props) {
prop.obj.setv({ [name]: value });
}
return this;
},
});
}
}
each(fn) {
let i = 0;
for (const model of this.models) {
fn(model, i++);
}
}
*[Symbol.iterator]() {
yield* this.models;
}
}
export class SubFigure extends GlyphAPI {
coordinates;
parent;
static __name__ = "SubFigure";
constructor(coordinates, parent) {
super();
this.coordinates = coordinates;
this.parent = parent;
}
_glyph(cls, method, positional, args, overrides) {
const { coordinates } = this;
return this.parent._glyph(cls, method, positional, args, { coordinates, ...overrides });
}
}
export class Figure extends BaseFigure {
static __name__ = "Figure";
get xaxes() {
return [...this.below, ...this.above].filter((r) => r instanceof Axis);
}
get yaxes() {
return [...this.left, ...this.right].filter((r) => r instanceof Axis);
}
get axes() {
return [...this.below, ...this.above, ...this.left, ...this.right].filter((r) => r instanceof Axis);
}
get xaxis() {
return new ModelProxy(this.xaxes);
}
get yaxis() {
return new ModelProxy(this.yaxes);
}
get axis() {
return new ModelProxy(this.axes);
}
get xgrids() {
return this.center.filter((r) => r instanceof Grid).filter((grid) => grid.dimension == 0);
}
get ygrids() {
return this.center.filter((r) => r instanceof Grid).filter((grid) => grid.dimension == 1);
}
get grids() {
return this.center.filter((r) => r instanceof Grid);
}
get xgrid() {
return new ModelProxy(this.xgrids);
}
get ygrid() {
return new ModelProxy(this.ygrids);
}
get grid() {
return new ModelProxy(this.grids);
}
get legend() {
const legends = this.panels.filter((r) => r instanceof Legend);
if (legends.length == 0) {
const legend = new Legend();
this.add_layout(legend);
return legend;
}
else {
const [legend] = legends;
return legend;
}
}
static {
extend(this, GlyphAPI);
}
constructor(attrs = {}) {
attrs = { ...attrs };
const x_axis_type = attrs.x_axis_type === undefined ? "auto" : attrs.x_axis_type;
const y_axis_type = attrs.y_axis_type === undefined ? "auto" : attrs.y_axis_type;
delete attrs.x_axis_type;
delete attrs.y_axis_type;
const x_minor_ticks = attrs.x_minor_ticks ?? "auto";
const y_minor_ticks = attrs.y_minor_ticks ?? "auto";
delete attrs.x_minor_ticks;
delete attrs.y_minor_ticks;
const x_axis_location = attrs.x_axis_location === undefined ? "below" : attrs.x_axis_location;
const y_axis_location = attrs.y_axis_location === undefined ? "left" : attrs.y_axis_location;
delete attrs.x_axis_location;
delete attrs.y_axis_location;
const x_axis_label = attrs.x_axis_label ?? "";
const y_axis_label = attrs.y_axis_label ?? "";
delete attrs.x_axis_label;
delete attrs.y_axis_label;
const x_range = Figure._get_range(attrs.x_range);
const y_range = Figure._get_range(attrs.y_range);
delete attrs.x_range;
delete attrs.y_range;
const x_scale = attrs.x_scale ?? Figure._get_scale(x_range, x_axis_type);
const y_scale = attrs.y_scale ?? Figure._get_scale(y_range, y_axis_type);
delete attrs.x_scale;
delete attrs.y_scale;
const { active_drag, active_inspect, active_scroll, active_tap, active_multi, } = attrs;
delete attrs.active_drag;
delete attrs.active_inspect;
delete attrs.active_scroll;
delete attrs.active_tap;
delete attrs.active_multi;
const tools = (() => {
const { tools, toolbar } = attrs;
if (tools != null) {
if (toolbar != null) {
throw new Error("'tools' and 'toolbar' can't be used together");
}
else {
delete attrs.tools;
if (isString(tools)) {
return tools.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
}
else {
return tools;
}
}
}
else {
return toolbar != null ? null : _default_tools;
}
})();
super({ ...attrs, x_range, y_range, x_scale, y_scale });
this._process_axis_and_grid(x_axis_type, x_axis_location, x_minor_ticks, x_axis_label, x_range, 0);
this._process_axis_and_grid(y_axis_type, y_axis_location, y_minor_ticks, y_axis_label, y_range, 1);
const tool_map = new Map();
if (tools != null) {
const resolved_tools = tools.map((tool) => {
if (tool instanceof Tool) {
return tool;
}
else {
const resolved_tool = Tool.from_string(tool);
tool_map.set(tool, resolved_tool);
return resolved_tool;
}
});
this.add_tools(...resolved_tools);
}
if (isString(active_drag) && active_drag != "auto") {
const tool = tool_map.get(active_drag);
if (tool instanceof GestureTool || tool instanceof ToolProxy) {
this.toolbar.active_drag = tool;
}
}
else if (active_drag !== undefined) {
this.toolbar.active_drag = active_drag;
}
if (isString(active_inspect) && active_inspect != "auto") {
const tool = tool_map.get(active_inspect);
if (tool != null) {
this.toolbar.active_inspect = tool;
}
}
else if (active_inspect !== undefined) {
this.toolbar.active_inspect = active_inspect;
}
if (isString(active_scroll) && active_scroll != "auto") {
const tool = tool_map.get(active_scroll);
if (tool instanceof GestureTool || tool instanceof ToolProxy) {
this.toolbar.active_scroll = tool;
}
}
else if (active_scroll !== undefined) {
this.toolbar.active_scroll = active_scroll;
}
if (isString(active_tap) && active_tap != "auto") {
const tool = tool_map.get(active_tap);
if (tool instanceof GestureTool || tool instanceof ToolProxy) {
this.toolbar.active_tap = tool;
}
}
else if (active_tap !== undefined) {
this.toolbar.active_tap = active_tap;
}
if (isString(active_multi) && active_multi != "auto") {
const tool = tool_map.get(active_multi);
if (tool instanceof GestureTool || tool instanceof ToolProxy) {
this.toolbar.active_multi = tool;
}
}
else if (active_multi !== undefined) {
this.toolbar.active_multi = active_multi;
}
}
get coordinates() {
return null;
}
subplot(coordinates) {
const mapping = new CoordinateMapping(coordinates);
return new SubFigure(mapping, this);
}
_pop_visuals(cls, props, prefix = "", defaults = {}, override_defaults = {}) {
const _split_feature_trait = function (ft) {
const fta = ft.split("_", 2);
return fta.length == 2 ? fta : fta.concat([""]);
};
const _is_visual = function (ft) {
const [feature, trait] = _split_feature_trait(ft);
return includes(["line", "fill", "hatch", "text", "global"], feature) && trait !== "";
};
defaults = { ...defaults };
const trait_defaults = {};
const props_proxy = dict(props);
const prototype_props_proxy = dict(cls.prototype._props);
const defaults_proxy = dict(defaults);
const trait_defaults_proxy = dict(trait_defaults);
const override_defaults_proxy = dict(override_defaults);
if (!defaults_proxy.has("text_color")) {
defaults.text_color = "black";
}
if (!defaults_proxy.has("hatch_color")) {
defaults.hatch_color = "black";
}
if (!trait_defaults_proxy.has("color")) {
trait_defaults.color = _default_color;
}
if (!trait_defaults_proxy.has("alpha")) {
trait_defaults.alpha = _default_alpha;
}
const result = {};
const traits = new Set();
for (const pname of keys(cls.prototype._props)) {
if (_is_visual(pname)) {
const trait = _split_feature_trait(pname)[1];
if (props_proxy.has(prefix + pname)) {
result[pname] = props[prefix + pname];
delete props[prefix + pname];
}
else if (!prototype_props_proxy.has(trait) && props_proxy.has(prefix + trait)) {
result[pname] = props[prefix + trait];
}
else if (override_defaults_proxy.has(trait)) {
result[pname] = override_defaults[trait];
}
else if (defaults_proxy.has(pname)) {
result[pname] = defaults[pname];
}
else if (trait_defaults_proxy.has(trait)) {
result[pname] = trait_defaults[trait];
}
if (!prototype_props_proxy.has(trait)) {
traits.add(trait);
}
}
}
for (const name of traits) {
delete props[prefix + name];
}
return result;
}
_find_uniq_name(data, name) {
let i = 1;
while (true) {
const new_name = `${name}__${i}`;
if (data.has(new_name)) {
i += 1;
}
else {
return new_name;
}
}
}
_fixup_values(cls, data, attrs) {
const unresolved_attrs = new Set();
const props = dict(cls.prototype._props);
for (const [name, value] of entries(attrs)) {
const prop = props.get(name);
if (prop != null) {
if (prop.type.prototype instanceof VectorSpec) {
if (value != null) {
if (isArray(value) || nd.is_NDArray(value)) {
let field;
if (data.has(name)) {
if (data.get(name) !== value) {
field = this._find_uniq_name(data, name);
data.set(field, value);
}
else {
field = name;
}
}
else {
field = name;
data.set(field, value);
}
attrs[name] = { field };
}
else if (isNumber(value) || isString(value)) { // or Date?
attrs[name] = { value };
}
}
if (prop.type.prototype instanceof UnitsSpec) {
const units_attr = `${name}_units`;
const units = attrs[units_attr];
if (units !== undefined) {
attrs[name] = { ...attrs[name], units };
unresolved_attrs.delete(units_attr);
delete attrs[units_attr];
}
}
}
}
else {
unresolved_attrs.add(name);
}
}
return unresolved_attrs;
}
_signature(method, positional) {
return `the method signature is ${method}(${positional.join(", ")}, args?)`;
}
_glyph(cls, method, positional, args, overrides = {}) {
let attrs;
const n_args = args.length;
const n_pos = positional.length;
if (n_args == n_pos || n_args == n_pos + 1) {
attrs = {};
for (const [[param, arg], i] of enumerate(zip(positional, args))) {
if (isPlainObject(arg) && !isVectorized(arg)) {
throw new Error(`invalid value for '${param}' parameter at position ${i}; ${this._signature(method, positional)}`);
}
else {
attrs[param] = arg;
}
}
if (n_args == n_pos + 1) {
const opts = args[n_args - 1];
if (!isPlainObject(opts) || isVectorized(opts)) {
throw new Error(`expected optional arguments; ${this._signature(method, positional)}`);
}
else {
attrs = { ...attrs, ...args[args.length - 1] };
}
}
}
else if (n_args == 0) {
attrs = {};
}
else if (n_args == 1) {
attrs = { ...args[0] };
}
else {
throw new Error(`wrong number of arguments; ${this._signature(method, positional)}`);
}
attrs = { ...attrs, ...overrides };
const source = (() => {
const { source } = attrs;
if (source == null) {
return new ColumnDataSource();
}
else if (source instanceof ColumnarDataSource) {
return source;
}
else {
return new ColumnDataSource({ data: source });
}
})();
const data = clone(source.data);
delete attrs.source;
const { view } = attrs;
delete attrs.view;
const legend = attrs.legend;
delete attrs.legend;
const legend_label = attrs.legend_label;
delete attrs.legend_label;
const legend_field = attrs.legend_field;
delete attrs.legend_field;
const legend_group = attrs.legend_group;
delete attrs.legend_group;
if ([legend, legend_label, legend_field, legend_group].filter((arg) => arg != null).length > 1) {
throw new Error("only one of legend, legend_label, legend_field, legend_group can be specified");
}
const name = attrs.name;
delete attrs.name;
const level = attrs.level;
delete attrs.level;
const visible = attrs.visible;
delete attrs.visible;
const x_range_name = attrs.x_range_name;
delete attrs.x_range_name;
const y_range_name = attrs.y_range_name;
delete attrs.y_range_name;
const coordinates = attrs.coordinates;
delete attrs.coordinates;
const glyph_ca = this._pop_visuals(cls, attrs);
const nglyph_ca = this._pop_visuals(cls, attrs, "nonselection_", glyph_ca, { alpha: 0.1 });
const sglyph_ca = this._pop_visuals(cls, attrs, "selection_", glyph_ca);
const hglyph_ca = this._pop_visuals(cls, attrs, "hover_", glyph_ca);
const mglyph_ca = this._pop_visuals(cls, attrs, "muted_", glyph_ca, { alpha: 0.2 });
const data_dict = dict(data);
this._fixup_values(cls, data_dict, glyph_ca);
this._fixup_values(cls, data_dict, nglyph_ca);
this._fixup_values(cls, data_dict, sglyph_ca);
this._fixup_values(cls, data_dict, hglyph_ca);
this._fixup_values(cls, data_dict, mglyph_ca);
this._fixup_values(cls, data_dict, attrs);
source.data = data;
const _make_glyph = (cls, attrs, extra_attrs) => {
return new cls({ ...attrs, ...extra_attrs });
};
const glyph = _make_glyph(cls, attrs, glyph_ca);
const nglyph = !is_empty(nglyph_ca) ? _make_glyph(cls, attrs, nglyph_ca) : "auto";
const sglyph = !is_empty(sglyph_ca) ? _make_glyph(cls, attrs, sglyph_ca) : "auto";
const hglyph = !is_empty(hglyph_ca) ? _make_glyph(cls, attrs, hglyph_ca) : undefined;
const mglyph = !is_empty(mglyph_ca) ? _make_glyph(cls, attrs, mglyph_ca) : "auto";
const glyph_renderer = new GlyphRenderer({
data_source: source,
view,
glyph,
nonselection_glyph: nglyph,
selection_glyph: sglyph,
hover_glyph: hglyph,
muted_glyph: mglyph,
name,
level,
visible,
x_range_name,
y_range_name,
coordinates,
});
if (legend_label != null) {
this._handle_legend_label(legend_label, this.legend, glyph_renderer);
}
if (legend_field != null) {
this._handle_legend_field(legend_field, this.legend, glyph_renderer);
}
if (legend_group != null) {
this._handle_legend_group(legend_group, this.legend, glyph_renderer);
}
this.add_renderers(glyph_renderer);
return glyph_renderer;
}
static _get_range(range) {
if (range == null) {
return new DataRange1d();
}
if (range instanceof Range) {
return range;
}
if (isArray(range)) {
if (isArrayOf(range, isString)) {
const factors = range;
return new FactorRange({ factors });
}
else {
const [start, end] = range;
return new Range1d({ start, end });
}
}
throw new Error(`unable to determine proper range for: '${range}'`);
}
static _get_scale(range_input, axis_type) {
if (range_input instanceof DataRange1d ||
range_input instanceof Range1d) {
switch (axis_type) {
case null:
case "auto":
case "linear":
case "datetime":
case "mercator":
return new LinearScale();
case "log":
return new LogScale();
}
}
if (range_input instanceof FactorRange) {
return new CategoricalScale();
}
throw new Error(`unable to determine proper scale for: '${range_input}'`);
}
_process_axis_and_grid(axis_type, axis_location, minor_ticks, axis_label, rng, dim) {
const axis = this._get_axis(axis_type, rng, dim);
if (axis != null) {
if (axis instanceof LogAxis) {
if (dim == 0) {
this.x_scale = new LogScale();
}
else {
this.y_scale = new LogScale();
}
}
if (axis.ticker instanceof ContinuousTicker) {
axis.ticker.num_minor_ticks = this._get_num_minor_ticks(axis, minor_ticks);
}
axis.axis_label = axis_label;
if (axis_location != null) {
this.add_layout(axis, axis_location);
}
const grid = new Grid({ dimension: dim, ticker: axis.ticker });
this.add_layout(grid);
}
}
_get_axis(axis_type, range, dim) {
switch (axis_type) {
case null:
return null;
case "linear":
return new LinearAxis();
case "log":
return new LogAxis();
case "datetime":
return new DatetimeAxis();
case "mercator": {
const axis = new MercatorAxis();
const dimension = dim == 0 ? "lon" : "lat";
axis.ticker.dimension = dimension;
axis.formatter.dimension = dimension;
return axis;
}
case "auto":
if (range instanceof FactorRange) {
return new CategoricalAxis();
}
else {
return new LinearAxis();
} // TODO: return DatetimeAxis (Date type)
default:
throw new Error("shouldn't have happened");
}
}
_get_num_minor_ticks(axis, num_minor_ticks) {
if (isNumber(num_minor_ticks)) {
if (num_minor_ticks <= 1) {
throw new Error("num_minor_ticks must be > 1");
}
else {
return num_minor_ticks;
}
}
else if (num_minor_ticks == null) {
return 0;
}
else {
return axis instanceof LogAxis ? 10 : 5;
}
}
_update_legend(legend_item_label, glyph_renderer) {
const { legend } = this;
let added = false;
for (const item of legend.items) {
if (item.label != null && is_equal(item.label, legend_item_label)) {
// XXX: remove this when vectorable properties are refined
const label = item.label;
if ("value" in label) {
item.renderers.push(glyph_renderer);
added = true;
break;
}
if ("field" in label && glyph_renderer.data_source == item.renderers[0].data_source) {
item.renderers.push(glyph_renderer);
added = true;
break;
}
}
}
if (!added) {
const new_item = new LegendItem({ label: legend_item_label, renderers: [glyph_renderer] });
legend.items.push(new_item);
}
}
_handle_legend_label(value, legend, glyph_renderer) {
const label = { value };
const item = this._find_legend_item(label, legend);
if (item != null) {
item.renderers.push(glyph_renderer);
}
else {
const new_item = new LegendItem({ label, renderers: [glyph_renderer] });
legend.items.push(new_item);
}
}
_handle_legend_field(field, legend, glyph_renderer) {
const label = { field };
const item = this._find_legend_item(label, legend);
if (item != null) {
item.renderers.push(glyph_renderer);
}
else {
const new_item = new LegendItem({ label, renderers: [glyph_renderer] });
legend.items.push(new_item);
}
}
_handle_legend_group(name, legend, glyph_renderer) {
const data = dict(glyph_renderer.data_source.data);
if (!data.has(name)) {
throw new Error(`column to be grouped does not exist in glyph data source: ${name}`);
}
const column = data.get(name) ?? [];
const values = uniq(column).sort();
for (const value of values) {
const label = { value: `${value}` };
const index = column.indexOf(value);
const new_item = new LegendItem({ label, renderers: [glyph_renderer], index });
legend.items.push(new_item);
}
}
_find_legend_item(label, legend) {
const cmp = new Comparator();
for (const item of legend.items) {
if (cmp.eq(item.label, label)) {
return item;
}
}
return null;
}
}
export function figure(attributes) {
return new Figure(attributes);
}
//# sourceMappingURL=figure.js.map