-
Notifications
You must be signed in to change notification settings - Fork 8
/
perspective_viewer.ts
241 lines (205 loc) · 9.92 KB
/
perspective_viewer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// Bokeh model for perspective-viewer
// See https://github.com/finos/perspective/tree/master/packages/perspective-viewer
// See https://docs.bokeh.org/en/latest/docs/reference/models/layouts.html
import { HTMLBox, HTMLBoxView } from "@bokehjs/models/layouts/html_box"
import {div} from "@bokehjs/core/dom"
// See https://docs.bokeh.org/en/latest/docs/reference/core/properties.html
import * as p from "@bokehjs/core/properties";
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source";
import {set_size, toAttribute, transform_cds_to_records} from "./shared";
const PERSPECTIVE_VIEWER_CLASSES = [
"perspective-viewer-material",
"perspective-viewer-material-dark",
"perspective-viewer-material-dense",
"perspective-viewer-material-dense-dark",
"perspective-viewer-vaporwave",
]
function is_not_perspective_class(item: any){
return !PERSPECTIVE_VIEWER_CLASSES.includes(item);
}
function theme_to_class(theme: string): string {
return "perspective-viewer-" + theme;
}
// The view of the Bokeh extension/ HTML element
// Here you can define how to render the model as well as react to model changes or View events.
export class PerspectiveViewerView extends HTMLBoxView {
model: PerspectiveViewer;
perspective_element: any;
connect_signals(): void {
super.connect_signals()
this.connect(this.model.source.properties.data.change, this.setData);
this.connect(this.model.source_stream.properties.data.change, this.addData);
this.connect(this.model.source_patch.properties.data.change, this.updateOrAddData);
this.connect(this.model.properties.columns.change, this.updateColumns)
this.connect(this.model.properties.parsed_computed_columns.change, this.updateParsedComputedColumns)
this.connect(this.model.properties.computed_columns.change, this.updateComputedColumns)
this.connect(this.model.properties.column_pivots.change, this.updateColumnPivots)
this.connect(this.model.properties.row_pivots.change, this.updateRowPivots)
this.connect(this.model.properties.aggregates.change, this.updateAggregates)
this.connect(this.model.properties.filters.change, this.updateFilters)
this.connect(this.model.properties.plugin.change, this.updatePlugin)
this.connect(this.model.properties.theme.change, this.updateTheme)
}
render(): void {
super.render()
const container = div({class: "pnx-perspective-viewer"});
container.innerHTML = this.getInnerHTML();
this.perspective_element=container.children[0]
set_size(container, this.model)
this.el.appendChild(container)
this.setData();
let viewer = this;
function handleConfigurationChange(this: any): void {
// this refers to the perspective-viewer element
// viewer refers to the PerspectiveViewerView element
viewer.model.columns = this.columns; // Note columns is available as a property
viewer.model.column_pivots = JSON.parse(this.getAttribute("column-pivots"));
viewer.model.parsed_computed_columns = JSON.parse(this.getAttribute("parsed-computed-columns"));
viewer.model.computed_columns = JSON.parse(this.getAttribute("computed-columns"));
viewer.model.row_pivots = JSON.parse(this.getAttribute("row-pivots"));
viewer.model.aggregates = JSON.parse(this.getAttribute("aggregates"));
viewer.model.sort = JSON.parse(this.getAttribute("sort"));
viewer.model.filters = JSON.parse(this.getAttribute("filters"));
// Perspective uses a plugin called 'debug' once in a while.
// We don't send this back to the python side
// Because then we would have to include it in the list of plugins
// the user can select from.
const plugin = this.getAttribute("plugin")
if (plugin!=="debug"){viewer.model.plugin = this.getAttribute("plugin")}
}
this.perspective_element.addEventListener("perspective-config-update", handleConfigurationChange)
}
private getInnerHTML() {
let innerHTML = "<perspective-viewer style='height:100%;width:100%;'";
innerHTML += toAttribute("class", theme_to_class(this.model.theme))
innerHTML += toAttribute("columns", this.model.columns)
innerHTML += toAttribute("column-pivots", this.model.column_pivots)
innerHTML += toAttribute("computed-columns", this.model.computed_columns)
innerHTML += toAttribute("row-pivots", this.model.row_pivots)
innerHTML += toAttribute("aggregates", this.model.aggregates)
innerHTML += toAttribute("sort", this.model.sort)
innerHTML += toAttribute("filters", this.model.filters)
innerHTML += toAttribute("plugin", this.model.plugin)
innerHTML += "></perspective-viewer>";
// We don't set the parsed-computed-columns
// It's not documented. Don't know if it is an internal thing?
// I think it gets generated from the computed-columns currently
// innerHTML += toAttribute("parsed-computed-columns", this.model.parsed_computed_columns)
return innerHTML;
}
setData(): void {
console.log("setData")
console.log(this.model.source.data)
let data = transform_cds_to_records(this.model.source);
this.perspective_element.load(data)
}
addData(): void {
// I need to find out how to only load the streamed data
// using this.perspective_element.update
console.log("addData")
this.setData();
}
updateOrAddData(): void {
// I need to find out how to only load the patched data
// using this.perspective_element.update
console.log("updateOrAddData")
this.setData();
}
updateAttribute(attribute: string, value: any, stringify: boolean): void {
// Might need som more testing/ a better understanding
// I'm not sure we should return here.
if (value === undefined || value===null || value === []) {
return;
}
const old_value = this.perspective_element.getAttribute(attribute);
if (stringify){
value = JSON.stringify(value);
}
// We should only set the attribute if the new value is different to old_value
// Otherwise we would get a recoursion/ stack overflow error
if (old_value!==value){
this.perspective_element.setAttribute(
attribute,
value
);
}
}
updateColumns(): void {this.updateAttribute("columns",this.model.columns,true,)}
updateParsedComputedColumns(): void {this.updateAttribute("parsed-computed-columns",this.model.parsed_computed_columns,true,)}
updateComputedColumns(): void {this.updateAttribute("computed-columns",this.model.computed_columns,true,)}
updateColumnPivots(): void {this.updateAttribute("column-pivots",this.model.column_pivots,true,)}
updateRowPivots(): void {this.updateAttribute("row-pivots",this.model.row_pivots,true,)}
updateAggregates(): void {this.updateAttribute("aggregates",this.model.row_pivots,true,)}
updateSort(): void {this.updateAttribute("sort",this.model.sort,true,)}
updateFilters(): void {this.updateAttribute("sort",this.model.filters,true,)}
updatePlugin(): void {this.updateAttribute("plugin",this.model.plugin,false,)}
updateTheme(): void {
// When you update the class attribute you have to be carefull
// For example when the user is dragging an element then 'dragging' is a part of the class attribute
let old_class = this.perspective_element.getAttribute("class");
let new_class = this.toNewClassAttribute(old_class, this.model.theme);
this.perspective_element.setAttribute("class", new_class)
}
/** Helper function to generate the new class attribute string
*
* If old_class = 'perspective-viewer-material dragging' and theme = 'material-dark'
* then 'perspective-viewer-material-dark dragging' is returned
*
* @param old_class For example 'perspective-viewer-material' or 'perspective-viewer-material dragging'
* @param theme The name of the new theme. For example 'material-dark'
*/
private toNewClassAttribute(old_class: any, theme: string): string {
let new_classes = [];
if (old_class != null) {
new_classes = old_class.split(" ").filter(is_not_perspective_class);
}
new_classes.push(theme_to_class(theme));
let new_class = new_classes.join(" ");
return new_class;
}
}
export namespace PerspectiveViewer {
export type Attrs = p.AttrsOf<Props>
export type Props = HTMLBox.Props & {
source: p.Property<ColumnDataSource>,
source_stream: p.Property<ColumnDataSource>,
source_patch: p.Property<ColumnDataSource>,
columns: p.Property<any[]>
parsed_computed_columns: p.Property<any[]>
computed_columns: p.Property<any[]>
column_pivots: p.Property<any[]>
row_pivots: p.Property<any[]>
aggregates: p.Property<any>
sort: p.Property<any[]>
filters: p.Property<any[]>
plugin: p.Property<any>
theme: p.Property<any>
}
}
export interface PerspectiveViewer extends PerspectiveViewer.Attrs { }
// The Bokeh .ts model corresponding to the Bokeh .py model
export class PerspectiveViewer extends HTMLBox {
properties: PerspectiveViewer.Props
constructor(attrs?: Partial<PerspectiveViewer.Attrs>) {
super(attrs)
}
static __module__ = "awesome_panel_extensions.bokeh_extensions.perspective_viewer"
static init_PerspectiveViewer(): void {
this.prototype.default_view = PerspectiveViewerView;
this.define<PerspectiveViewer.Props>({
source: [p.Any, ],
source_stream: [p.Any, ],
source_patch: [p.Any, ],
columns: [p.Array, ],
parsed_computed_columns: [p.Array, []],
computed_columns: [p.Array, ],
column_pivots: [p.Array, ],
row_pivots: [p.Array, ],
aggregates: [p.Any, ],
sort: [p.Array,],
filters: [p.Array, ],
plugin: [p.String, ],
theme: [p.String, ],
})
}
}