import {ModelEvent} from "@bokehjs/core/bokeh_events"
import type {StyleSheetLike} from "@bokehjs/core/dom"
import {div} from "@bokehjs/core/dom"
import type * as p from "@bokehjs/core/properties"
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"
import {HTMLBox, HTMLBoxView, set_size} from "./layout"
import type {Attrs} from "@bokehjs/core/types"
import perspective_css from "styles/models/perspective.css"
const THEMES: any = {
"pro-dark": "Pro Dark",
pro: "Pro Light",
vaporwave: "Vaporwave",
solarized: "Solarized",
"solarized-dark": "Solarized Dark",
monokai: "Monokai",
}
const PLUGINS: any = {
datagrid: "Datagrid",
d3_x_bar: "X Bar",
d3_y_bar: "Y Bar",
d3_xy_line: "X/Y Line",
d3_y_line: "Y Line",
d3_y_area: "Y Area",
d3_y_scatter: "Y Scatter",
d3_xy_scatter: "X/Y Scatter",
d3_treemap: "Treemap",
d3_candlestick: "Candlestick",
d3_sunburst: "Sunburst",
d3_heatmap: "Heatmap",
d3_ohlc: "OHLC",
}
function objectFlip(obj: any) {
const ret: any = {}
Object.keys(obj).forEach(key => {
ret[obj[key]] = key
})
return ret
}
const PLUGINS_REVERSE = objectFlip(PLUGINS)
const THEMES_REVERSE = objectFlip(THEMES)
export class PerspectiveClickEvent extends ModelEvent {
constructor(readonly config: any, readonly column_names: string[], readonly row: any[]) {
super()
}
protected override get event_values(): Attrs {
return {model: this.origin, config: this.config, column_names: this.column_names, row: this.row}
}
static {
this.prototype.event_name = "perspective-click"
}
}
export class PerspectiveView extends HTMLBoxView {
declare model: Perspective
perspective_element: any
table: any
worker: any
_updating: boolean = false
_config_listener: any = null
_current_config: any = null
_current_plugin: string | null = null
_loaded: boolean = false
_plugin_configs: any = new Map()
override connect_signals(): void {
super.connect_signals()
this.connect(this.model.source.properties.data.change, () => this.setData())
this.connect(this.model.source.streaming, () => this.stream())
this.connect(this.model.source.patching, () => this.patch())
const {
schema, columns, expressions, split_by, group_by,
aggregates, filters, sort, plugin, selectable, editable, theme,
title, settings,
} = this.model.properties
const not_updating = (fn: () => void) => {
return () => {
if (this._updating) {
return
}
fn()
}
}
this.on_change(schema, () => {
this.worker.table(this.model.schema).then((table: any) => {
this.table = table
this.table.update(this.data)
this.perspective_element.load(this.table)
})
})
this.on_change(columns, not_updating(() => {
this.perspective_element.restore({columns: this.model.columns})
}))
this.on_change(expressions, not_updating(() => {
this.perspective_element.restore({expressions: this.model.expressions})
}))
this.on_change(split_by, not_updating(() => {
this.perspective_element.restore({split_by: this.model.split_by})
}))
this.on_change(group_by, not_updating(() => {
this.perspective_element.restore({group_by: this.model.group_by})
}))
this.on_change(aggregates, not_updating(() => {
this.perspective_element.restore({aggregates: this.model.aggregates})
}))
this.on_change(filters, not_updating(() => {
this.perspective_element.restore({filter: this.model.filters})
}))
this.on_change(settings, not_updating(() => {
this.perspective_element.restore({settings: this.model.settings})
}))
this.on_change(title, not_updating(() => {
this.perspective_element.restore({title: this.model.title})
}))
this.on_change(sort, not_updating(() => {
this.perspective_element.restore({sort: this.model.sort})
}))
this.on_change(plugin, not_updating(() => {
this.perspective_element.restore({plugin: PLUGINS[this.model.plugin], ...settings})
}))
this.on_change(selectable, not_updating(() => {
this.perspective_element.restore({plugin_config: {...this._current_config, selectable: this.model.selectable}})
}))
this.on_change(editable, not_updating(() => {
this.perspective_element.restore({plugin_config: {...this._current_config, editable: this.model.editable}})
}))
this.on_change(theme, not_updating(() => {
this.perspective_element.restore({theme: THEMES[this.model.theme as string]}).catch(() => {})
}))
}
override disconnect_signals(): void {
if (this._config_listener != null) {
this.perspective_element.removeEventListener("perspective-config-update", this._config_listener)
}
this._config_listener = null
super.disconnect_signals()
}
override remove(): void {
if (this.perspective_element) {
this.perspective_element.delete(() => this.worker.terminate())
}
super.remove()
}
override stylesheets(): StyleSheetLike[] {
return [...super.stylesheets(), perspective_css]
}
override render(): void {
super.render()
this.worker = (window as any).perspective.worker()
const container = div({
class: "pnx-perspective-viewer",
style: {
zIndex: "0",
},
})
this._current_plugin = this.model.plugin
container.innerHTML = ""
this.perspective_element = container.children[0]
const themesArray = Object.values(THEMES)
const filteredThemes = themesArray.filter(t => t !== this.model.theme)
const orderedThemes = [this.model.theme, ...filteredThemes]
this.perspective_element.resetThemes(orderedThemes).catch(() => {})
set_size(container, this.model)
this.shadow_el.appendChild(container)
this.worker.table(this.model.schema).then((table: any) => {
this.table = table
this.table.update(this.data)
this.perspective_element.load(this.table)
const plugin_config = {
...this.model.plugin_config,
editable: this.model.editable,
selectable: this.model.selectable,
}
this.perspective_element.restore({
aggregates: this.model.aggregates,
columns: this.model.columns,
columns_config: this.model.columns_config,
expressions: this.model.expressions,
filter: this.model.filters,
split_by: this.model.split_by,
group_by: this.model.group_by,
plugin: PLUGINS[this.model.plugin],
plugin_config,
settings: this.model.settings,
sort: this.model.sort,
theme: THEMES[this.model.theme ],
title: this.model.title,
}).catch(() => {})
this.perspective_element.save().then((config: any) => {
this._current_config = config
})
this._config_listener = () => this.sync_config()
this.perspective_element.addEventListener("perspective-config-update", this._config_listener)
this.perspective_element.addEventListener("perspective-click", (event: any) => {
this.model.trigger_event(new PerspectiveClickEvent(event.detail.config, event.detail.column_names, event.detail.row))
})
this._loaded = true
})
}
sync_config(): boolean {
if (this._updating) {
return true
}
this.perspective_element.save().then((config: any) => {
if (config.plugin !== this._current_plugin) {
this._plugin_configs.set(this._current_plugin, {
columns: this._current_config.columns,
columns_config: this._current_config.columns_config,
plugin_config: this._current_config.plugin_config,
})
if (this._plugin_configs.has(config.plugin)) {
const overrides = this._plugin_configs.get(config.plugin)
this.perspective_element.restore(overrides)
config = {...config, ...overrides}
}
}
this._current_config = config
this._current_plugin = config.plugin
const props: any = {}
for (let option in config) {
let value = config[option]
if (value === undefined || (option == "plugin" && value === "debug") || option == "version" || this.model.properties.hasOwnProperty(option) === undefined) {
continue
}
if (option === "filter") {
option = "filters"
} else if (option === "plugin") {
value = PLUGINS_REVERSE[value ]
} else if (option === "theme") {
value = THEMES_REVERSE[value ]
}
props[option] = value
}
this._updating = true
this.model.setv(props)
this._updating = false
})
return true
}
get data(): any {
const data: any = {}
for (const column of this.model.source.columns()) {
let array = this.model.source.get_array(column)
if (this.model.schema[column] == "datetime" && array.includes(-9223372036854776)) {
array = array.map((v) => v === -9223372036854776 ? null : v)
}
data[column] = array
}
return data
}
setData(): void {
if (!this._loaded) {
return
}
for (const col of this.model.source.columns()) {
if (!(col in this.model.schema)) {
return
}
}
this.table.replace(this.data)
}
stream(): void {
if (this._loaded) {
this.table.replace(this.data)
}
}
patch(): void {
if (this._loaded) {
this.table.replace(this.data)
}
}
}
export namespace Perspective {
export type Attrs = p.AttrsOf
export type Props = HTMLBox.Props & {
aggregates: p.Property
split_by: p.Property
columns: p.Property
columns_config: p.Property
expressions: p.Property
editable: p.Property
filters: p.Property
group_by: p.Property
plugin: p.Property
plugin_config: p.Property
selectable: p.Property
schema: p.Property
settings: p.Property
sort: p.Property
source: p.Property
theme: p.Property
title: p.Property
}
}
export interface Perspective extends Perspective.Attrs { }
export class Perspective extends HTMLBox {
declare properties: Perspective.Props
constructor(attrs?: Partial) {
super(attrs)
}
static override __module__ = "panel.models.perspective"
static {
this.prototype.default_view = PerspectiveView
this.define(({Any, List, Bool, Ref, Nullable, Str}) => ({
aggregates: [ Any, {} ],
columns: [ List(Nullable(Str)), [] ],
columns_config: [ Any, {} ],
expressions: [ Any, {} ],
split_by: [ Nullable(List(Str)), null ],
editable: [ Bool, true ],
filters: [ Nullable(List(Any)), null ],
group_by: [ Nullable(List(Str)), null ],
plugin: [ Str ],
plugin_config: [ Any, {} ],
selectable: [ Bool, true ],
settings: [ Bool, true ],
schema: [ Any, {} ],
sort: [ Nullable(List(List(Str))), null ],
source: [ Ref(ColumnDataSource) ],
theme: [ Str, "pro" ],
title: [ Nullable(Str), null ],
}))
}
}