Skip to content

Commit 14203bf

Browse files
authored
Merge pull request #413 from jpmorganchase/port-js
Port View to C++
2 parents feb31f3 + 079f9bc commit 14203bf

File tree

11 files changed

+582
-94
lines changed

11 files changed

+582
-94
lines changed

API.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Perspective API design notes
2+
3+
### Goals
4+
- Make the core C++ library an extensible base for developers to build on using different languages.
5+
- Port features previously segregated to Javascript into C++, thereby removing dependency on JS for the core library.
6+
- Move business logic into C++, thus allowing the purpose of the JS library to act as a bound wrapper rather than a dependent extension.
7+
8+
This document aims to elaborate on the design of the core library API, as well as information designed to aid the core developer in creating new features/extending the library into new language bindings.
9+
10+
### Components
11+
- Table: C++ class that will implement the features previously found in the `perspective.js` table, including all prototype methods and class members.
12+
- t_table: C++ class that manages the underlying operations for `Table`.
13+
- View: C++ class that will implement all features and members previously found in the `perspective.js` view.
14+
- pool: A collection of `gnode`(s) managed in C++.
15+
- gnode: A "graph node" that contains any number of `t_table`s, although in implementation we only assign one `t_table` to one `gnode`.
16+
17+
### Steps for porting
18+
1. Transliterate JS to C++: liberal use of `embind` and `val` in order to start porting features previously segregated to Javascript into C++.
19+
2. Make C++ functions portable: start removing the use of all JS-dependent data structures, members, and methods in order to allow for C++ portability.
20+
3. Add the translation layer: now that C++ functions expect generic C++ parameters (and not a `val`, on which any number of arbitrary JS-only operations may be called), add the translator layer that converts JS input into what the portable C++ API expects, as well as converts the output into something suitable for JS.
21+
4. Convert the JS library into simple function calls: though the `perspective.js` API will not change, the underlying implementation for methods will simply return the output of the C++ method, with translation where necessary.
22+
23+
### C++
24+
`View`: class which will implement all methods from the `perspective.js` version of view.
25+
- Expects C++ data structure input and returns native data structures.
26+
- Should be as portable and extensible as possible.
27+
- Should abstract away concepts like pool and gnode.
28+
29+
`Table`: class which implements all methods from the `perspective.js` table.
30+
- Expects C++ data structure input and returns native data structures.
31+
- Should be as portable and extensible as possible.
32+
33+
### Translation/Helper layer
34+
`make_view`: helper function in `emscripten.cpp` that returns a `std::shared_ptr<View<CONTEXT_T>>`.
35+
- Should construct the underlying `View` object.
36+
- Should expect native C++ data structures as input.
37+
38+
*TBD: how should we implement the translation layer?*
39+
- As separate methods that are called, which makes `make_view` extremely dependent on side-effects?
40+
- As a layer in `perspective.js`, which makes it harder to create C++ native data structures?
41+
- As a new binding in the C++ `src` folder, allowing for a place for developers to add their own language bindings?
42+
43+
### Javascript
44+
`View`: the view class on the Javascript side should be a wrapper around calls to the Emscripten `__MODULE__`.
45+
- Should maintain an internal reference to the C++ `View` object created on instantiation.
46+
- Methods should call the corresponding method on the C++ `View`, and return output that is ready for JS consumption.

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ set (SOURCE_FILES
325325
src/cpp/tree_context_common.cpp
326326
src/cpp/utils.cpp
327327
src/cpp/update_task.cpp
328+
src/cpp/view.cpp
328329
src/cpp/vocab.cpp
329330
)
330331

packages/perspective/src/js/perspective.js

Lines changed: 31 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import * as defaults from "./defaults.js";
1111
import {DataAccessor, clean_data} from "./DataAccessor/DataAccessor.js";
1212
import {DateParser} from "./DataAccessor/DateParser.js";
13+
import {extract_map, extract_vector} from "./translator.js";
1314
import {bindall, get_column_type} from "./utils.js";
1415

1516
import {Precision} from "@apache-arrow/es5-esm/type";
@@ -228,6 +229,16 @@ export default function(Module) {
228229
this.callbacks = callbacks;
229230
this.name = name;
230231
this.table = table;
232+
233+
this._View = undefined;
234+
if (sides === 0) {
235+
this._View = __MODULE__.make_view_zero(pool, ctx, sides, gnode, name, defaults.COLUMN_SEPARATOR_STRING);
236+
} else if (sides === 1) {
237+
this._View = __MODULE__.make_view_one(pool, ctx, sides, gnode, name, defaults.COLUMN_SEPARATOR_STRING);
238+
} else if (sides === 2) {
239+
this._View = __MODULE__.make_view_two(pool, ctx, sides, gnode, name, defaults.COLUMN_SEPARATOR_STRING);
240+
}
241+
231242
bindall(this);
232243
}
233244

@@ -237,8 +248,10 @@ export default function(Module) {
237248
* they are garbage collected - you must call this method to reclaim these.
238249
*/
239250
view.prototype.delete = async function() {
240-
this.pool.unregister_context(this.gnode.get_id(), this.name);
251+
this._View.delete_view();
252+
this._View.delete();
241253
this.ctx.delete();
254+
242255
this.table.views.splice(this.table.views.indexOf(this), 1);
243256
this.table = undefined;
244257
let i = 0,
@@ -266,38 +279,15 @@ export default function(Module) {
266279
};
267280

268281
view.prototype._column_names = function(skip_depth = false) {
269-
let col_names = [];
270-
let aggs = this.ctx.get_column_names();
271-
for (let key = 0; key < this.ctx.unity_get_column_count(); key++) {
272-
let col_name;
273-
if (this.sides() === 0) {
274-
col_name = aggs.get(key);
275-
if (col_name === "psp_okey") {
276-
continue;
277-
}
278-
} else {
279-
let name = aggs.get(key % aggs.size()).name();
280-
if (name === "psp_okey") {
281-
continue;
282-
}
283-
let col_path = this.ctx.unity_get_column_path(key + 1);
284-
if (skip_depth && col_path.size() < skip_depth) {
285-
col_path.delete();
286-
continue;
287-
}
288-
col_name = [];
289-
for (let cnix = 0; cnix < col_path.size(); cnix++) {
290-
col_name.push(__MODULE__.scalar_vec_to_val(col_path, cnix));
291-
}
292-
col_name = col_name.reverse();
293-
col_name.push(name);
294-
col_name = col_name.join(defaults.COLUMN_SEPARATOR_STRING);
295-
col_path.delete();
296-
}
297-
col_names.push(col_name);
282+
let skip = false,
283+
depth = 0;
284+
285+
if (skip_depth !== false) {
286+
skip = true;
287+
depth = Number(skip_depth);
298288
}
299-
aggs.delete();
300-
return col_names;
289+
290+
return extract_vector(this._View._column_names(skip, depth));
301291
};
302292

303293
/**
@@ -312,42 +302,14 @@ export default function(Module) {
312302
* @returns {Promise<Object>} A Promise of this {@link view}'s schema.
313303
*/
314304
view.prototype.schema = async function() {
315-
// get type mapping
316-
let schema = this.gnode.get_tblschema();
317-
let _types = schema.types();
318-
let names = schema.columns();
319-
schema.delete();
305+
let new_schema = extract_map(this._View.schema());
320306

321-
let types = {};
322-
for (let i = 0; i < names.size(); i++) {
323-
types[names.get(i)] = _types.get(i).value;
324-
}
325-
let new_schema = {};
326-
let col_names = this._column_names();
327-
for (let col_name of col_names) {
328-
col_name = col_name.split(defaults.COLUMN_SEPARATOR_STRING);
329-
col_name = col_name[col_name.length - 1];
330-
if (types[col_name] === 1 || types[col_name] === 2) {
331-
new_schema[col_name] = "integer";
332-
} else if (types[col_name] === 19) {
333-
new_schema[col_name] = "string";
334-
} else if (types[col_name] === 9 || types[col_name] === 10) {
335-
new_schema[col_name] = "float";
336-
} else if (types[col_name] === 11) {
337-
new_schema[col_name] = "boolean";
338-
} else if (types[col_name] === 12) {
339-
new_schema[col_name] = "datetime";
340-
} else if (types[col_name] === 13) {
341-
new_schema[col_name] = "date";
342-
}
307+
for (let name in new_schema) {
343308
if (this.sides() > 0 && this.config.row_pivot.length > 0) {
344-
new_schema[col_name] = map_aggregate_types(col_name, new_schema[col_name], this.config.aggregate);
309+
new_schema[name] = map_aggregate_types(name, new_schema[name], this.config.aggregate);
345310
}
346311
}
347312

348-
_types.delete();
349-
names.delete();
350-
351313
return new_schema;
352314
};
353315

@@ -571,7 +533,7 @@ export default function(Module) {
571533
* @returns {Promise<number>} The number of aggregated rows.
572534
*/
573535
view.prototype.num_rows = async function() {
574-
return this.ctx.get_row_count();
536+
return this._View.num_rows();
575537
};
576538

577539
/**
@@ -584,7 +546,7 @@ export default function(Module) {
584546
* @returns {Promise<number>} The number of aggregated columns.
585547
*/
586548
view.prototype.num_columns = async function() {
587-
return this.ctx.unity_get_column_count();
549+
return this._View.num_columns();
588550
};
589551

590552
/**
@@ -595,7 +557,7 @@ export default function(Module) {
595557
* @returns {Promise<bool>} Whether this row is expanded.
596558
*/
597559
view.prototype.get_row_expanded = async function(idx) {
598-
return this.ctx.unity_get_row_expanded(idx);
560+
return this._View.get_row_expanded(idx);
599561
};
600562

601563
/**
@@ -606,11 +568,7 @@ export default function(Module) {
606568
* @returns {Promise<void>}
607569
*/
608570
view.prototype.expand = async function(idx) {
609-
if (this.nsides === 2 && this.ctx.unity_get_row_depth(idx) < this.config.row_pivot.length) {
610-
return this.ctx.open(__MODULE__.t_header.HEADER_ROW, idx);
611-
} else if (this.nsides < 2) {
612-
return this.ctx.open(idx);
613-
}
571+
return this._View.expand(idx);
614572
};
615573

616574
/**
@@ -621,27 +579,15 @@ export default function(Module) {
621579
* @returns {Promise<void>}
622580
*/
623581
view.prototype.collapse = async function(idx) {
624-
if (this.nsides === 2) {
625-
return this.ctx.close(__MODULE__.t_header.HEADER_ROW, idx);
626-
} else {
627-
return this.ctx.close(idx);
628-
}
582+
return this._View.collapse(idx);
629583
};
630584

631585
/**
632586
* Set expansion `depth` pf the pivot tree.
633587
*
634588
*/
635589
view.prototype.set_depth = async function(depth) {
636-
if (this.config.row_pivot.length >= depth) {
637-
if (this.nsides === 2) {
638-
return this.ctx.set_depth(__MODULE__.t_header.HEADER_ROW, depth);
639-
} else {
640-
return this.ctx.set_depth(depth);
641-
}
642-
} else {
643-
console.warn(`Cannot expand past ${this.config.row_pivot.length}`);
644-
}
590+
return this._View.set_depth(depth, this.config.row_pivot.length);
645591
};
646592

647593
/**
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/******************************************************************************
2+
*
3+
* Copyright (c) 2019, the Perspective Authors.
4+
*
5+
* This file is part of the Perspective library, distributed under the terms of
6+
* the Apache License 2.0. The full license can be found in the LICENSE file.
7+
*
8+
*/
9+
10+
/** Translation layer
11+
* Interface between C++ and JS to handle conversions/data structures that
12+
* were previously handled in non-portable perspective.js
13+
*/
14+
export const extract_vector = function(vector) {
15+
let extracted = [];
16+
for (let i = 0; i < vector.size(); i++) {
17+
let item = vector.get(i);
18+
extracted.push(item);
19+
}
20+
vector.delete();
21+
return extracted;
22+
};
23+
24+
export const extract_map = function(map) {
25+
let extracted = {};
26+
let keys = map.keys();
27+
for (let i = 0; i < keys.size(); i++) {
28+
let key = keys.get(i);
29+
extracted[key] = map.get(key);
30+
}
31+
map.delete();
32+
keys.delete();
33+
return extracted;
34+
};

packages/perspective/test/js/constructors.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,15 @@ module.exports = perspective => {
679679
table.delete();
680680
});
681681

682+
it("Handles utf16 column names", async function() {
683+
var table = perspective.table({š: [1, 2, 3]});
684+
let view = table.view({});
685+
let result = await view.schema();
686+
expect(result).toEqual({š: "integer"});
687+
view.delete();
688+
table.delete();
689+
});
690+
682691
it("Handles utf16", async function() {
683692
var table = perspective.table(data_6);
684693
let view = table.view({});

packages/perspective/test/js/pivots.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ module.exports = perspective => {
322322
it("['x'] does not translate type when only pivoted by column", async function() {
323323
var table = perspective.table(data);
324324
var view = table.view({
325-
col_pivot: ["y"],
325+
column_pivot: ["y"],
326326
aggregate: [{column: "x", op: "avg"}]
327327
});
328328
let result2 = await view.schema();

0 commit comments

Comments
 (0)