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