UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

245 lines 9.74 kB
import { ContextWhich, Location, TimedeltaResolutionType } from "../../core/enums"; import { assert } from "../../core/util/assert"; import { sprintf } from "../../core/util/templating"; import { isString, isArray, isBoolean, is_undefined } from "../../core/util/types"; import { TickFormatter } from "./tick_formatter"; import { ONE_DAY, ONE_HOUR, ONE_MICRO, ONE_MILLI, ONE_MINUTE, ONE_NANO, ONE_SECOND } from "../tickers/util"; // Labels of time units, from finest to coarsest. export const resolution_order = [ "nanoseconds", "microseconds", "milliseconds", "seconds", "minsec", "minutes", "hourmin", "hours", "days", ]; export const formatting_map = { "%NS": (t) => _ns(t, 1_000), "%ns": (t) => _ns(t, null), "%US": (t) => _us(t, 1_000), "%us": (t) => _us(t, null), "%MS": (t) => _ms(t, 1_000), "%ms": (t) => _ms(t, null), "%S": (t) => _seconds(t, 60), "%s": (t) => _seconds(t, null), "%M": (t) => _minutes(t, 60), "%m": (t) => _minutes(t, null), "%H": (t) => _hours(t, 24), "%h": (t) => _hours(t, null), "%d": (t) => _days(t, null), }; export function _get_resolution(resolution_secs, span_secs) { // Our resolution boundaries should not be round numbers, because we want // them to fall between the possible tick intervals (which *are* round // numbers, as we've worked hard to ensure). Consequently, we adjust the // resolution upwards a small amount (less than any possible step in // scales) to make the effective boundaries slightly lower. const adjusted_ms = resolution_secs * 1.1 * 1000; const span_ms = span_secs * 1000; if (adjusted_ms < ONE_MICRO) { return "nanoseconds"; } if (adjusted_ms < ONE_MILLI) { return "microseconds"; } if (adjusted_ms < ONE_SECOND) { return "milliseconds"; } if (adjusted_ms < ONE_MINUTE) { return span_ms >= ONE_MINUTE ? "minsec" : "seconds"; } if (adjusted_ms < ONE_HOUR) { return span_ms >= ONE_HOUR ? "hourmin" : "minutes"; } if (adjusted_ms < ONE_DAY) { return "hours"; } return "days"; } export function _str_timedelta(t, format) { for (const [k, v] of Object.entries(formatting_map)) { const format_template = new RegExp(`((^|[^%])(%%)*)${k}`); if (format_template.test(format)) { format = format.replace(format_template, `$1${v(t)}`); } } return format; } export function _days(t, factor_next) { const days = _calc_tick_value(t, ONE_DAY, factor_next); return _str_tick_value(days, factor_next); } export function _hours(t, factor_next) { const hours = _calc_tick_value(t, ONE_HOUR, factor_next); return _str_tick_value(hours, factor_next); } export function _minutes(t, factor_next) { const minutes = _calc_tick_value(t, ONE_MINUTE, factor_next); return _str_tick_value(minutes, factor_next); } export function _seconds(t, factor_next) { const seconds = _calc_tick_value(t, ONE_SECOND, factor_next); return _str_tick_value(seconds, factor_next); } export function _ms(t, factor_next) { const millis = _calc_tick_value(t, ONE_MILLI, factor_next); return _str_tick_value(millis, factor_next); } export function _us(t, factor_next) { const us = _calc_tick_value(t, ONE_MICRO, factor_next); return _str_tick_value(us, factor_next); } export function _ns(t, factor_next) { const ns = _calc_tick_value(t, ONE_NANO, factor_next); return _str_tick_value(ns, factor_next); } function _str_tick_value(v, factor_next) { return factor_next !== null ? sprintf(`%0${`${factor_next - 1}`.length}d`, v) : String(v); } function _calc_tick_value(t, factor_transform, factor_next) { if (factor_next !== null) { return _time_since_last_next(t, factor_transform, factor_next); } else { return _time_total(t, factor_transform); } } function _time_since_last_next(t, factor_transform, factor_next) { if (factor_transform < 1) { // sub milliseconds // handle floating point precision as best as possible const t_nano = Math.round(t * 1_000_000); const divisor_next = factor_transform * factor_next * 1_000_000; // switch to String to avoid precision issues, // e.g. 116011933670718300 - 116011933670718000 equals 304 const digits = `${divisor_next}`.length; const str_t_nano = String(t_nano); const nanos_since_last_next = parseFloat(str_t_nano.substring(str_t_nano.length - digits)) % divisor_next; return (nanos_since_last_next / (factor_transform * 1_000_000)) % factor_next; } const millis_since_last_next = t % (factor_transform * factor_next); return millis_since_last_next / factor_transform; } function _time_total(t, factor_transform) { return Math.floor(t / factor_transform); } export class TimedeltaTickFormatter extends TickFormatter { static __name__ = "TimedeltaTickFormatter"; constructor(attrs) { super(attrs); } static { this.define(({ Bool, Nullable, Or, Ref, Str, Arrayable }) => ({ nanoseconds: [Str, "%NSns"], microseconds: [Str, "%USus"], milliseconds: [Str, "%MSms"], seconds: [Str, "%H:%M:%S"], minsec: [Str, "%H:%M:%S"], minutes: [Str, "%H:%M"], hourmin: [Str, "%H:%M"], hours: [Str, "%H:%M"], days: [Str, "%d days"], strip_leading_zeros: [Or(Bool, Arrayable(TimedeltaResolutionType)), false], hide_repeats: [Bool, false], context: [Nullable(Or(Str, Ref(TimedeltaTickFormatter))), null], context_which: [ContextWhich, "start"], context_location: [Location, "below"], })); } doFormat(ticks, _opts, _resolution) { if (ticks.length == 0) { return []; } const span = Math.abs(ticks[ticks.length - 1] - ticks[0]) / 1000.0; const r = span / (ticks.length - 1); const resolution = is_undefined(_resolution) ? _get_resolution(r, span) : _resolution; let base_labels = []; for (const tick of ticks) { const base_label = this._compute_label(tick, resolution); base_labels.push(base_label); } if (this.hide_repeats) { base_labels = this._hide_repeating_labels(base_labels); } if (this.context == null) { return base_labels; } const context_labels = this._compute_context_labels(ticks, resolution); return this._build_full_labels(base_labels, context_labels); } _compute_label(t, resolution) { const s0 = _str_timedelta(t, this[resolution]); const { strip_leading_zeros } = this; if ((isBoolean(strip_leading_zeros) && strip_leading_zeros) || (isArray(strip_leading_zeros) && strip_leading_zeros.includes(resolution))) { const ss = s0.replace(/^0+/g, ""); if (ss != s0 && !Number.isInteger(Number(ss[0]))) { // If the string can now be parsed as starting with an integer, then // leave all zeros stripped, otherwise start with a zero. return `0${ss}`; } return ss; } return s0; } _compute_context_labels(ticks, resolution) { const { context } = this; assert(context != null); const context_labels = []; if (isString(context)) { for (const tick of ticks) { context_labels.push(_str_timedelta(tick, context)); } } else { context_labels.push(...context.doFormat(ticks, { loc: 0 }, resolution)); } const which = this.context_which; const N = context_labels.length; for (let i = 0; i < context_labels.length; i++) { if ((which == "start" && i != 0) || (which == "end" && i != N - 1) || (which == "center" && i != Math.floor(N / 2))) { context_labels[i] = ""; } } return context_labels; } _build_full_labels(base_labels, context_labels) { const loc = this.context_location; const full_labels = []; if (context_labels.every(v => v === "")) { return base_labels; } for (let i = 0; i < base_labels.length; i++) { const label = base_labels[i]; const context = context_labels[i]; // In case of above and below blank strings are not trimmed in order to // keep the same visual format across all ticks. const full_label = (() => { switch (loc) { case "above": return `${context}\n${label}`; case "below": return `${label}\n${context}`; case "left": return context == "" ? label : `${context} ${label}`; case "right": return context == "" ? label : `${label} ${context}`; } })(); full_labels.push(full_label); } return full_labels; } _hide_repeating_labels(labels) { // For repeating labels only utilize the first entry if (labels.length <= 1) { return labels; } const labels_h = [labels[0]]; let index_first_entry = 0; for (let i = 1; i < labels.length; i++) { if (labels[index_first_entry] == labels[i]) { labels_h.push(""); } else { labels_h.push(labels[i]); index_first_entry = i; } } return labels_h; } } //# sourceMappingURL=timedelta_tick_formatter.js.map