Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for formatters to ValueRef #13650

Open
wants to merge 4 commits into
base: branch-3.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions bokehjs/src/lib/core/build_views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,21 @@ export function remove_views(view_storage: ViewStorage<HasProps>): void {
view_storage.delete(model)
}
}

export function traverse_views(views: View[], fn: (view: View) => void): void {
const visited = new Set<View>()
const queue: View[] = [...views]

while (true) {
const view = queue.shift()
if (view === undefined) {
break
}
if (visited.has(view)) {
continue
}
visited.add(view)
queue.push(...view.children())
fn(view)
}
}
3 changes: 3 additions & 0 deletions bokehjs/src/lib/core/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const HatchPatternType = Enum(
)
export type HatchPatternType = typeof HatchPatternType["__type__"]

export const BuiltinFormatter = Enum("raw", "basic", "numeral", "printf", "datetime")
export type BuiltinFormatter = typeof BuiltinFormatter["__type__"]

export const HTTPMethod = Enum("POST", "GET")
export type HTTPMethod = typeof HTTPMethod["__type__"]

Expand Down
34 changes: 17 additions & 17 deletions bokehjs/src/lib/core/util/templating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ import type {CustomJSHover} from "models/tools/inspectors/customjs_hover"
import {sprintf as sprintf_js} from "sprintf-js"
import tz from "timezone"
import type {Dict} from "../types"
import {Enum} from "../kinds"
import type {BuiltinFormatter} from "../enums"
import {logger} from "../logging"
import {dict} from "./object"
import {is_NDArray} from "./ndarray"
import {isArray, isNumber, isString, isTypedArray} from "./types"

export const FormatterType = Enum("numeral", "printf", "datetime")
export type FormatterType = typeof FormatterType["__type__"]
const {abs} = Math

export type FormatterSpec = CustomJSHover | FormatterType
export type FormatterSpec = CustomJSHover | BuiltinFormatter
export type Formatters = Dict<FormatterSpec>
export type FormatterFunc = (value: unknown, format: string, special_vars: Vars) => string
export type Index = number | ImageIndex
export type Vars = {[key: string]: unknown}

export const DEFAULT_FORMATTERS = {
numeral: (value: unknown, format: string, _special_vars: Vars) => Numbro.format(value, format),
datetime: (value: unknown, format: string, _special_vars: Vars) => tz(value, format),
printf: (value: unknown, format: string, _special_vars: Vars) => sprintf(format, value),
export const DEFAULT_FORMATTERS: {[key in BuiltinFormatter]: FormatterFunc} = {
raw: (value: unknown, _format: string, _special_vars: Vars) => `${value}`,
basic: (value: unknown, format: string, special_vars: Vars) => basic_formatter(value, format, special_vars),
numeral: (value: unknown, format: string, _special_vars: Vars) => Numbro.format(value, format),
datetime: (value: unknown, format: string, _special_vars: Vars) => tz(value, format),
printf: (value: unknown, format: string, _special_vars: Vars) => sprintf(format, value),
}

export function sprintf(format: string, ...args: unknown[]): string {
Expand All @@ -33,13 +34,12 @@ export function sprintf(format: string, ...args: unknown[]): string {
export function basic_formatter(value: unknown, _format: string, _special_vars: Vars): string {
if (isNumber(value)) {
const format = (() => {
switch (false) {
case Math.floor(value) != value:
return "%d"
case !(Math.abs(value) > 0.1) || !(Math.abs(value) < 1000):
return "%0.3f"
default:
return "%0.3e"
if (Number.isInteger(value)) {
return "%d"
} else if (0.1 < abs(value) && abs(value) < 1000) {
return "%0.3f"
} else {
return "%0.3e"
}
})()

Expand All @@ -52,7 +52,7 @@ export function basic_formatter(value: unknown, _format: string, _special_vars:
export function get_formatter(spec: string, format?: string, formatters?: Formatters): FormatterFunc {
// no format, use default built in formatter
if (format == null) {
return basic_formatter
return DEFAULT_FORMATTERS.basic
}

// format spec in the formatters dict, use that
Expand All @@ -77,7 +77,7 @@ export function get_formatter(spec: string, format?: string, formatters?: Format
return DEFAULT_FORMATTERS.numeral
}

const MISSING = "???"
export const MISSING = "???"

function _get_special_value(name: string, special_vars: Vars) {
if (name in special_vars) {
Expand Down
8 changes: 5 additions & 3 deletions bokehjs/src/lib/models/dom/color_ref.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {ValueRef, ValueRefView} from "./value_ref"
import type {Formatters} from "./placeholder"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index as DataIndex} from "core/util/templating"
import type {Index} from "core/util/templating"
import {_get_column_value} from "core/util/templating"
import {span} from "core/dom"
import type {PlainObject} from "core/types"
import type * as p from "core/properties"
import * as styles from "styles/tooltips.css"

Expand All @@ -22,9 +24,9 @@ export class ColorRefView extends ValueRefView {
this.el.appendChild(this.swatch_el)
}

override update(source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void {
override update(source: ColumnarDataSource, i: Index | null, _vars: PlainObject, _formatters?: Formatters): void {
const value = _get_column_value(this.model.field, source, i)
const text = value == null ? "???" : `${value}` //.toString()
const text = value == null ? "???" : `${value}`
this.el.textContent = text
}
}
Expand Down
6 changes: 4 additions & 2 deletions bokehjs/src/lib/models/dom/index_.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {Placeholder, PlaceholderView} from "./placeholder"
import type {Formatters} from "./placeholder"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index as DataIndex} from "core/util/templating"
import type {PlainObject} from "core/types"
import type * as p from "core/properties"

export class IndexView extends PlaceholderView {
declare model: Index

update(_source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void {
this.el.textContent = i == null ? "(null)" : i.toString()
update(_source: ColumnarDataSource, i: DataIndex | null, _vars: PlainObject, _formatters?: Formatters): void {
this.el.textContent = i == null ? "(null)" : `${i}`
}
}

Expand Down
25 changes: 16 additions & 9 deletions bokehjs/src/lib/models/dom/placeholder.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import {DOMNode, DOMNodeView} from "./dom_node"
import {DOMElement, DOMElementView} from "./dom_element"
import {CustomJS} from "../callbacks/customjs"
import {CustomJSHover} from "../tools/inspectors/customjs_hover"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index as DataIndex} from "core/util/templating"
import type * as p from "core/properties"
import type {Dict, PlainObject} from "core/types"
import {BuiltinFormatter} from "core/enums"
import {Or, Ref} from "core/kinds"

export abstract class PlaceholderView extends DOMNodeView {
export const Formatter = Or(BuiltinFormatter, Ref(CustomJS), Ref(CustomJSHover))
export type Formatter = typeof Formatter["__type__"]

export type Formatters = Dict<Formatter>

export abstract class PlaceholderView extends DOMElementView {
declare model: Placeholder
static override tag_name = "span" as const

override render(): void {
// XXX: no implementation?
}
static override tag_name = "span" as const

abstract update(source: ColumnarDataSource, i: DataIndex | null, vars: object/*, formatters?: Formatters*/): void
abstract update(source: ColumnarDataSource, i: DataIndex | null, vars: PlainObject<unknown>, formatters?: Formatters): void
}

export namespace Placeholder {
export type Attrs = p.AttrsOf<Props>
export type Props = DOMNode.Props
export type Props = DOMElement.Props
}

export interface Placeholder extends Placeholder.Attrs {}

export abstract class Placeholder extends DOMNode {
export abstract class Placeholder extends DOMElement {
declare properties: Placeholder.Props
declare __view_type__: PlaceholderView

Expand Down
24 changes: 9 additions & 15 deletions bokehjs/src/lib/models/dom/template.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {DOMElement, DOMElementView} from "./dom_element"
import {Action} from "./action"
import {PlaceholderView} from "./placeholder"
import type {Formatters} from "./placeholder"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index as DataIndex} from "core/util/templating"
import type {Index} from "core/util/templating"
import type {ViewStorage, IterViews} from "core/build_views"
import {build_views, remove_views} from "core/build_views"
import {build_views, remove_views, traverse_views} from "core/build_views"
import type {PlainObject} from "core/types"
import type * as p from "core/properties"

export class TemplateView extends DOMElementView {
declare model: Template
static override tag_name = "div" as const

readonly action_views: ViewStorage<Action> = new Map()

Expand All @@ -28,19 +29,12 @@ export class TemplateView extends DOMElementView {
super.remove()
}

update(source: ColumnarDataSource, i: DataIndex | null, vars: object = {}/*, formatters?: Formatters*/): void {
function descend(obj: DOMElementView): void {
for (const child of obj.child_views.values()) {
if (child instanceof PlaceholderView) {
child.update(source, i, vars)
} else if (child instanceof DOMElementView) {
descend(child)
}
update(source: ColumnarDataSource, i: Index | null, vars: PlainObject, formatters?: Formatters): void {
traverse_views([this], (view) => {
if (view instanceof PlaceholderView) {
view.update(source, i, vars, formatters)
}
}

descend(this)

})
for (const action of this.action_views.values()) {
action.update(source, i, vars)
}
Expand Down
14 changes: 6 additions & 8 deletions bokehjs/src/lib/models/dom/value_of.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {DOMNode, DOMNodeView} from "./dom_node"
import {DOMElement, DOMElementView} from "./dom_element"
import {HasProps} from "core/has_props"
import {empty} from "core/dom"
import {to_string} from "core/util/pretty"
import type * as p from "core/properties"

export class ValueOfView extends DOMNodeView {
export class ValueOfView extends DOMElementView {
declare model: ValueOf
declare el: HTMLElement

override connect_signals(): void {
super.connect_signals()
Expand All @@ -17,8 +15,8 @@ export class ValueOfView extends DOMNodeView {
}
}

render(): void {
empty(this.el)
override render(): void {
super.render()
this.el.style.display = "contents"

const text = (() => {
Expand All @@ -37,15 +35,15 @@ export class ValueOfView extends DOMNodeView {

export namespace ValueOf {
export type Attrs = p.AttrsOf<Props>
export type Props = DOMNode.Props & {
export type Props = DOMElement.Props & {
obj: p.Property<HasProps>
attr: p.Property<string>
}
}

export interface ValueOf extends ValueOf.Attrs {}

export class ValueOf extends DOMNode {
export class ValueOf extends DOMElement {
declare properties: ValueOf.Props
declare __view_type__: ValueOfView

Expand Down
57 changes: 49 additions & 8 deletions bokehjs/src/lib/models/dom/value_ref.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import {Placeholder, PlaceholderView} from "./placeholder"
import {Placeholder, PlaceholderView, Formatter} from "./placeholder"
import type {Formatters} from "./placeholder"
import {CustomJS} from "../callbacks/customjs"
import {CustomJSHover} from "../tools/inspectors/customjs_hover"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index as DataIndex} from "core/util/templating"
import {_get_column_value} from "core/util/templating"
import type {Index} from "core/util/templating"
import {_get_column_value, MISSING, DEFAULT_FORMATTERS} from "core/util/templating"
import {execute} from "core/util/callbacks"
import {isArray} from "core/util/types"
import type * as p from "core/properties"
import type {PlainObject} from "core/types"

export class ValueRefView extends PlaceholderView {
declare model: ValueRef

update(source: ColumnarDataSource, i: DataIndex | null, _vars: object/*, formatters?: Formatters*/): void {
const value = _get_column_value(this.model.field, source, i)
const text = value == null ? "???" : `${value}` //.toString()
this.el.textContent = text
update(source: ColumnarDataSource, i: Index | null, vars: PlainObject, _formatters?: Formatters): void {
const {field, format, formatter} = this.model
const value = _get_column_value(field, source, i)

const render = (output: unknown) => {
if (output == null) {
this.el.textContent = MISSING
} else if (output instanceof Node) {
this.el.replaceChildren(output)
} else if (isArray(output)) {
this.el.replaceChildren(...output.map((item) => item instanceof Node ? item : `${item}`))
} else {
this.el.textContent = `${output}`
}
}

if (formatter instanceof CustomJS) {
void (async () => {
const output = await execute(formatter, this.model, {value, format, vars})
render(output)
})()
} else {
const output = (() => {
if (format == null) {
return DEFAULT_FORMATTERS.basic(value, "", vars)
} else {
if (formatter instanceof CustomJSHover) {
return formatter.format(value, format, vars)
} else {
return DEFAULT_FORMATTERS[formatter](value, format, vars)
}
}
})()
render(output)
}
}
}

export namespace ValueRef {
export type Attrs = p.AttrsOf<Props>
export type Props = Placeholder.Props & {
field: p.Property<string>
format: p.Property<string | null>
formatter: p.Property<Formatter>
}
}

Expand All @@ -33,8 +72,10 @@ export class ValueRef extends Placeholder {

static {
this.prototype.default_view = ValueRefView
this.define<ValueRef.Props>(({Str}) => ({
this.define<ValueRef.Props>(({Str, Nullable}) => ({
field: [ Str ],
format: [ Nullable(Str), null ],
formatter: [ Formatter, "raw" ],
}))
}
}