import {div} from "@bokehjs/core/dom" import type * as p from "@bokehjs/core/properties" import {ModelEvent} from "@bokehjs/core/bokeh_events" import {isArray} from "@bokehjs/core/util/types" import {LayoutDOM, LayoutDOMView} from "@bokehjs/models/layouts/layout_dom" import type {Attrs} from "@bokehjs/core/types" import {set_size} from "./layout" import {debounce} from "debounce" export class VegaEvent extends ModelEvent { constructor(readonly data: any) { super() } protected override get event_values(): Attrs { return {model: this.origin, data: this.data} } static { this.prototype.event_name = "vega_event" } } export class VegaPlotView extends LayoutDOMView { declare model: VegaPlot vega_view: any container: HTMLDivElement _callbacks: string[] _connected: string[] _replot: any _resize: any _rendered: boolean = false override connect_signals(): void { super.connect_signals() const {data, show_actions, theme, data_sources, events} = this.model.properties this._replot = debounce(() => this._plot(), 20) this.on_change([data, show_actions, theme], () => { this._replot() }) this.on_change(data_sources, () => this._connect_sources()) this.on_change(events, () => { for (const event of this.model.events) { if (this._callbacks.indexOf(event) > -1) { continue } this._callbacks.push(event) const callback = (name: string, value: any) => this._dispatch_event(name, value) const timeout = this.model.throttle[event] || 20 this.vega_view.addSignalListener(event, debounce(callback, timeout, false)) } }) this._connected = [] this._connect_sources() } _connect_sources(): void { for (const ds in this.model.data_sources) { const cds = this.model.data_sources[ds] if (this._connected.indexOf(ds) < 0) { this.connect(cds.properties.data.change, () => this._replot()) this._connected.push(ds) } } } override remove(): void { if (this.vega_view) { this.vega_view.finalize() } super.remove() } _dispatch_event(name: string, value: any): void { if ("vlPoint" in value && value.vlPoint.or != null) { const indexes = [] for (const index of value.vlPoint.or) { if (index._vgsid_ !== undefined) { // If "_vgsid_" property exists indexes.push(index._vgsid_) } else { // If "_vgsid_" property doesn't exist // Iterate through all properties in the "index" object for (const key in index) { if (index.hasOwnProperty(key)) { // To ensure key comes from "index" object itself, not its prototype indexes.push({[key]: index[key]}) // Push a new object with this key-value pair into the array } } } } value = indexes } this.model.trigger_event(new VegaEvent({type: name, value})) } _fetch_datasets() { const datasets: any = {} for (const ds in this.model.data_sources) { const cds = this.model.data_sources[ds] const data: any = [] const columns = cds.columns() for (let i = 0; i < cds.get_length(); i++) { const item: any = {} for (const column of columns) { item[column] = cds.data[column][i] } data.push(item) } datasets[ds] = data } return datasets } get child_models(): LayoutDOM[] { return [] } override render(): void { super.render() this._rendered = false this.container = div() set_size(this.container, this.model) this._callbacks = [] this._plot() this.shadow_el.append(this.container) } _plot(): void { const data = this.model.data if ((data == null) || !(window as any).vegaEmbed) { return } if (this.model.data_sources && (Object.keys(this.model.data_sources).length > 0)) { const datasets = this._fetch_datasets() if ("data" in datasets) { data.data.values = datasets.data delete datasets.data } if (data.data != null) { const data_objs = isArray(data.data) ? data.data : [data.data] for (const d of data_objs) { if (d.name in datasets) { d.values = datasets[d.name] delete datasets[d.name] } } } this.model.data.datasets = datasets } const config: any = {actions: this.model.show_actions, theme: this.model.theme}; (window as any).vegaEmbed(this.container, this.model.data, config).then((result: any) => { this.vega_view = result.view this._resize = debounce(() => this.resize_view(result.view), 50) const callback = (name: string, value: any) => this._dispatch_event(name, value) for (const event of this.model.events) { this._callbacks.push(event) const timeout = this.model.throttle[event] || 20 this.vega_view.addSignalListener(event, debounce(callback, timeout, false)) } }) } override after_layout(): void { super.after_layout() if (this.vega_view != null) { this._resize() } } resize_view(view: any): void { const canvas = view._renderer.canvas() if (!this._rendered && canvas !== null) { for (const listener of view._eventListeners) { if (listener.type === "resize") { listener.handler(new Event("resize")) } } this._rendered = true } } } export namespace VegaPlot { export type Attrs = p.AttrsOf export type Props = LayoutDOM.Props & { data: p.Property data_sources: p.Property events: p.Property show_actions: p.Property theme: p.Property throttle: p.Property } } export interface VegaPlot extends VegaPlot.Attrs {} export class VegaPlot extends LayoutDOM { declare properties: VegaPlot.Props constructor(attrs?: Partial) { super(attrs) } static override __module__ = "panel.models.vega" static { this.prototype.default_view = VegaPlotView this.define(({Any, List, Bool, Nullable, Str}) => ({ data: [ Any, {} ], data_sources: [ Any, {} ], events: [ List(Str), [] ], show_actions: [ Bool, false ], theme: [ Nullable(Str), null ], throttle: [ Any, {} ], })) } }