UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

269 lines 9.38 kB
import { Range } from "./range"; import { PaddingUnits } from "../../core/enums"; import { Or, Str, List, Tuple } from "../../core/kinds"; import * as p from "../../core/properties"; import { Signal0 } from "../../core/signaling"; import { ScreenArray } from "../../core/types"; import { every, sum } from "../../core/util/array"; import { isArray, isNumber, isString } from "../../core/util/types"; export const Factor = Or(Str, Tuple(Str, Str), Tuple(Str, Str, Str)); export const FactorSeq = Or(List(Str), List(Tuple(Str, Str)), List(Tuple(Str, Str, Str))); export function map_one_level(factors, padding, offset = 0) { const mapping = new Map(); for (let i = 0; i < factors.length; i++) { const factor = factors[i]; if (mapping.has(factor)) { throw new Error(`duplicate factor or subfactor: ${factor}`); } mapping.set(factor, { value: 0.5 + i * (1 + padding) + offset }); } const inner_padding = (factors.length - 1) * padding; return { mapping, inner_padding }; } export function map_two_levels(factors, outer_pad, factor_pad, offset = 0) { const mapping = new Map(); const tops = new Map(); for (const [f0, f1] of factors) { const top = tops.get(f0) ?? []; tops.set(f0, [...top, f1]); } let suboffset = offset; let total_subpad = 0; for (const [f0, top] of tops) { const n = top.length; const sub = map_one_level(top, factor_pad, suboffset); total_subpad += sub.inner_padding; const subtot = sum(top.map((f1) => sub.mapping.get(f1).value)); mapping.set(f0, { value: subtot / n, mapping: sub.mapping }); suboffset += n + outer_pad + sub.inner_padding; } const inner_padding = (tops.size - 1) * outer_pad + total_subpad; return { mapping, tops: [...mapping.keys()], inner_padding }; } export function map_three_levels(factors, outer_pad, inner_pad, factor_pad, offset = 0) { const mapping = new Map(); const tops = new Map(); for (const [f0, f1, f2] of factors) { const top = tops.get(f0) ?? []; tops.set(f0, [...top, [f1, f2]]); } let suboffset = offset; let total_subpad = 0; for (const [f0, top] of tops) { const n = top.length; const sub = map_two_levels(top, inner_pad, factor_pad, suboffset); total_subpad += sub.inner_padding; const subtot = sum(top.map(([f1]) => sub.mapping.get(f1).value)); mapping.set(f0, { value: subtot / n, mapping: sub.mapping }); suboffset += n + outer_pad + sub.inner_padding; } const mids = []; for (const [f0, L2] of mapping) { for (const f1 of L2.mapping.keys()) { mids.push([f0, f1]); } } const inner_padding = (tops.size - 1) * outer_pad + total_subpad; return { mapping, tops: [...mapping.keys()], mids, inner_padding }; } const is_l1 = (x) => isString(x); const is_l2 = (x) => isArray(x) && x.length == 2 && isString(x[0]) && isString(x[1]); const is_l3 = (x) => isArray(x) && x.length == 3 && isString(x[0]) && isString(x[1]) && isString(x[2]); export class FactorMapper { static __name__ = "FactorMapper"; levels; mids; tops; inner_padding; mapping; constructor({ levels, mapping, tops = null, mids = null, inner_padding }) { this.levels = levels; this.mapping = mapping; this.tops = tops; this.mids = mids; this.inner_padding = inner_padding; } static compute_levels(factors) { if (every(factors, is_l1)) { return 1; } if (every(factors, is_l2)) { return 2; } if (every(factors, is_l3)) { return 3; } throw TypeError("factor levels are inconsistent"); } static for(range) { switch (this.compute_levels(range.factors)) { case 1: { return new L1FactorMapper(range); } case 2: { return new L2FactorMapper(range); } case 3: { return new L3FactorMapper(range); } } } map(x) { if (isNumber(x)) { return x; } const [boxed, offset] = (() => { if (isString(x)) { return [[x], 0]; } const last = x[x.length - 1]; if (isNumber(last)) { return [x.slice(0, -1), last]; } return [x, 0]; })(); if (boxed.length > this.levels) { throw new Error(`Attempted to map ${boxed.length} levels of factors with an L${this.levels}FactorMap`); } return this.lookup_value(boxed) + offset; } lookup_value(x) { return this.lookup_entry(x)?.value ?? NaN; } } class L1FactorMapper extends FactorMapper { static __name__ = "L1FactorMapper"; constructor(range) { const { factors, factor_padding } = range; const spec = map_one_level(factors, factor_padding); super({ levels: 1, ...spec }); } lookup_entry(x) { const [f0] = x; return this.mapping.get(f0) ?? null; } } class L2FactorMapper extends FactorMapper { static __name__ = "L2FactorMapper"; constructor(range) { const { factors, group_padding, factor_padding } = range; const spec = map_two_levels(factors, group_padding, factor_padding); super({ levels: 2, ...spec }); } lookup_entry(x) { if (x.length == 1) { const [f0] = x; return this.mapping.get(f0) ?? null; } else { const [f0, f1] = x; return this.mapping.get(f0)?.mapping.get(f1) ?? null; } } } class L3FactorMapper extends FactorMapper { static __name__ = "L3FactorMapper"; constructor(range) { const { factors, group_padding, subgroup_padding, factor_padding } = range; const spec = map_three_levels(factors, group_padding, subgroup_padding, factor_padding); super({ levels: 3, ...spec }); } lookup_entry(x) { if (x.length == 1) { const [f0] = x; return this.mapping.get(f0) ?? null; } else if (x.length == 2) { const [f0, f1] = x; return this.mapping.get(f0)?.mapping.get(f1) ?? null; } else { const [f0, f1, f2] = x; return this.mapping.get(f0)?.mapping.get(f1)?.mapping.get(f2) ?? null; } } } export class FactorRange extends Range { static __name__ = "FactorRange"; constructor(attrs) { super(attrs); } static { this.define(({ Float }) => ({ factors: [FactorSeq, []], factor_padding: [Float, 0], subgroup_padding: [Float, 0.8], group_padding: [Float, 1.4], range_padding: [Float, 0], range_padding_units: [PaddingUnits, "percent"], start: [Float, p.unset, { readonly: true }], end: [Float, p.unset, { readonly: true }], })); } mapper; get min() { return this.start; } get max() { return this.end; } initialize() { super.initialize(); this.configure(); } connect_signals() { super.connect_signals(); this.connect(this.properties.factors.change, () => this.reset()); this.connect(this.properties.factor_padding.change, () => this.reset()); this.connect(this.properties.group_padding.change, () => this.reset()); this.connect(this.properties.subgroup_padding.change, () => this.reset()); this.connect(this.properties.range_padding.change, () => this.reset()); this.connect(this.properties.range_padding_units.change, () => this.reset()); } invalidate_synthetic = new Signal0(this, "invalidate_synthetic"); reset() { this.configure(); this.invalidate_synthetic.emit(); } /** Convert a categorical factor into a synthetic coordinate. */ synthetic(x) { return this.mapper.map(x); } /** Convert an array of categorical factors into synthetic coordinates. */ v_synthetic(xs) { return ScreenArray.from(xs, (x) => this.synthetic(x)); } /** Convert a synthetic coordinate into a categorical factor. */ factor(x) { for (const f of this.factors) { const v = this.mapper.map(f); if (x >= (v - 0.5) && x < (v + 0.5)) { return f; } } return null; } compute_bounds(inner_padding) { const interval = this.factors.length + inner_padding; const padding = (() => { switch (this.range_padding_units) { case "percent": { return interval * this.range_padding / 2; } case "absolute": { return this.range_padding; } } })(); return [-padding, interval + padding]; } configure() { this.mapper = FactorMapper.for(this); const [start, end] = this.compute_bounds(this.mapper.inner_padding); this.setv({ start, end }, { silent: true }); if (this.bounds == "auto") { this._computed_bounds = [start, end]; } } } //# sourceMappingURL=factor_range.js.map