Skip to content

Commit

Permalink
Merge branch 'd3-wrapper' into d3-wrapper-dev-oliver
Browse files Browse the repository at this point in the history
  • Loading branch information
Oliver-BE committed Jul 13, 2020
2 parents fb45fd4 + 8f2fa3e commit 06249e2
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 7 deletions.
7 changes: 7 additions & 0 deletions source/web/d3/d3_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ namespace internal {
}, this->id);
}
};

/// Catch-all object for storing references to things created in JS
class JSObject : public D3_Base {
public:
JSObject() {;};
};

}

#endif
208 changes: 208 additions & 0 deletions source/web/d3/dataset.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* @note This file is part of Empirical, https://github.com/devosoft/Empirical
* @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md
* @date 2016-2020
*
* @file dataset.h
* @brief Tools to maintain data in D3.
*/

#ifndef EMP_D3_LOAD_DATA_H
#define EMP_D3_LOAD_DATA_H

#include <functional>

#include "d3_init.h"

#include "../JSWrap.h"
#include "../js_utils.h"
#include "../../tools/string_utils.h"

namespace D3 {

class Dataset : public D3_Base {
public:
Dataset() {;}
Dataset(int i) : D3_Base(i) {;}

template <typename T>
emp::sfinae_decoy<double, decltype(&T::operator())>
Min(T comp) {
const uint32_t fun_id = emp::JSWrap(comp, "", false);

double min = EM_ASM_DOUBLE({
return d3.min(emp_d3.objects[$0], function(d) {return emp.Callback($1, d);});
}, this->id, fun_id);

emp::JSDelete(fun_id);

return min;
}

template <typename T>
emp::sfinae_decoy<double, decltype(&T::operator())>
Max(T comp) {
const uint32_t fun_id = emp::JSWrap(comp, "", false);

double max = EM_ASM_DOUBLE({
return d3.max(emp_d3.objects[$0], function(d) {return emp.Callback($1, d);});
}, this->id, fun_id);

emp::JSDelete(fun_id);

return max;
}

};

// TODO: Support init field for loading data
// struct RequestInit {
// EMP_BUILD_INTROSPECTIVE_TUPLE( std::string, RequestMode,
// std::string, RequestCache
// )
// };

class CSVDataset : public Dataset {
public:
CSVDataset() {;}
CSVDataset(int i) : Dataset(i) {;}

void LoadDataFromFile(const std::string & location, const std::string & row_callback, bool header=true) {
emp_assert(
EM_ASM_INT({
return emp_d3.is_function(UTF8ToString($0));
}, row_callback.c_str()),
"Row callback specify an actual function in Javascript."
);

EM_ASM({
const location = UTF8ToString($0);
const row_callback_str = UTF8ToString($1);
const id = $2;
const header = $3;

var row_callback_func = emp_d3.find_function(row_callback_string);
if (header) {
d3.csv(location, row_callback_func).then(function(data) {
emp_d3.objects[id] = data;
});
} else {
d3.text(location).then(function(data) {
emp_d3.objects[id] = d3.csvParseRows(data, row_callback_func);
});
}
}, location.c_str(), row_callback.c_str(), this->id, header);
}

/// Put the last row of the array into arr
template <size_t N, typename T>
void GetLastRow(emp::array<T, N> & arr, int n) {
emp_assert(EM_ASM_INT({return emp_d3.objects[$0].length > n;}, GetID(), n));

EM_ASM({
emp_i.__outgoing_array = emp_d3.objects[$0][$1];
}, GetID(), n);
emp::pass_array_to_cpp(arr);
}

};

class JSONDataset : public Dataset {
public:

JSONDataset(int i) : Dataset(i) {;}
JSONDataset() {
EM_ASM({emp_d3.objects[$0] = [];}, this->id);
};

void LoadDataFromFile(const std::string & filename) {
EM_ASM({
d3.json(UTF8ToString($1)).then(function(data){
emp_d3.objects[$0] = data;
});
}, this->id, filename.c_str());
}

template <typename DATA_TYPE>
void LoadDataFromFile(const std::string & filename, std::function<void(DATA_TYPE)> fun) {
emp::JSWrap(fun, "__json_load_fun__"+emp::to_string(id), true);

EM_ASM({
d3.json(UTF8ToString($1)).then(function(data){
emp_d3.objects[$0] = data;
emp["__json_load_fun__"+$0](data);
});
}, this->id, filename.c_str());
}

void LoadDataFromFile(const std::string & filename, std::function<void(void)> fun) {
emp::JSWrap(fun, "__json_load_fun__"+emp::to_string(id), true);

EM_ASM({
var filename = UTF8ToString($1);
d3.json(filename).then(function(data){
emp_d3.objects[$0] = data;
emp["__json_load_fun__"+$0]();
});
}, this->id, filename.c_str());
}

void Append(const std::string & json) {
EM_ASM({
emp_d3.objects[$0].push(JSON.parse(UTF8ToString($1)));
}, this->id, json.c_str());
}

void AppendNested(const std::string & json) {

int fail = EM_ASM_INT({
var obj = JSON.parse(UTF8ToString($1));

var result = null;
for (var i in emp_d3.objects[$0]) {
result = emp_d3.find_in_hierarchy(emp_d3.objects[$0][i], obj.parent);
if (result) {
break;
}
}
if (!result) {
return 1;
}
result.children.push(obj);
return 0;
}, this->id, json.c_str());

if (fail) {
emp::NotifyWarning("Append to JSON failed - parent not found");
}
}

// Appends into large trees can be sped up by maintaining a list of
// possible parent nodes
int AppendNestedFromList(const std::string & json, JSObject & options) {
int pos = EM_ASM_INT({
var parent_node = null;
var pos = -1;
var child_node = JSON.parse(UTF8ToString($1));
for (var item in emp_d3.objects[$0]) {
if (emp_d3.objects[$0][item].name == child_node.parent) {
parent_node = emp_d3.objects[$0][item];
pos = item;
break;
}
}

if (!parent_node.hasOwnProperty("children")){
parent_node.children = [];
}
parent_node.children.push(child_node);
return pos;
}, options.GetID(), json.c_str());

return pos;
}

};
}

#endif
21 changes: 21 additions & 0 deletions source/web/d3/library_d3.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ var D3Library = {
return (typeof window[func_name] === "function");
},

// Inspired by Niels' answer to
// http://stackoverflow.com/questions/12899609/how-to-add-an-object-to-a-nested-javascript-object-using-a-parent-id/37888800#37888800
find_in_hierarchy: function(root, id) {
if (root.name == id) {
return root;
}
if (root.children) {
for (var k in root.children) {
if (root.children[k].name == id) {
return root.children[k];
}
else if (root.children[k].children) {
result = this.find_in_hierarchy(root.children[k], id);
if (result) {
return result;
}
}
}
}
},

},

// TODO: Rename to InitializeEmpD3?
Expand Down
7 changes: 3 additions & 4 deletions source/web/d3/selection.h
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ namespace internal {
const func_name_str = UTF8ToString($1);
const attr_name_str = UTF8ToString($2);
const value = emp_d3.find_function(func_name_str);
emp_d3.objects[new_id] = emp_d3.objects[id].attr(attr_name_str, value);
emp_d3.objects[id].attr(attr_name_str, value);
}, this->id, value.c_str(), name.c_str());

return *(static_cast<DERIVED *>(this));
Expand Down Expand Up @@ -279,7 +279,7 @@ namespace internal {
const func_name_str = UTF8ToString($1);
const attr_name_str = UTF8ToString($2);
const value = emp_d3.find_function(func_name_str);
emp_d3.objects[new_id] = emp_d3.objects[id].attr(attr_name_str, value);
emp_d3.objects[id].attr(attr_name_str, value);
}, this->id, value, name.c_str());

return *(static_cast<DERIVED *>(this));
Expand Down Expand Up @@ -835,8 +835,7 @@ namespace D3 {

/// Append DOM element(s) of the type specified by [name] to this selection.
Selection Append(const std::string & name) {
const int new_id =internal::NextD3ID();

const int new_id = internal::NextD3ID();
EM_ASM({
var new_selection = emp_d3.objects[$0].append(UTF8ToString($1));
emp_d3.objects[$2] = new_selection;
Expand Down
60 changes: 57 additions & 3 deletions tests/web/d3/selection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
// - empty selection
// - non empty selection

struct Test_Selection : emp::web::BaseTest {
// Test the Selection constructors
struct Test_SelectionConstruction : emp::web::BaseTest {
D3::Selection empty_selection;
D3::Selection svg_selection;
D3::Selection circle_selection;

Test_Selection() : emp::web::BaseTest({"emp_test_container"}) {
Test_SelectionConstruction() : emp::web::BaseTest({"emp_test_container"}) {
std::cout << empty_selection.GetID() << std::endl;
EM_ASM({
$("#emp_test_container").append("<svg id='test_svg'><circle/><circle/></svg>");
Expand Down Expand Up @@ -82,6 +83,58 @@ struct Test_Selection : emp::web::BaseTest {
}
};

// enter, append, etc
struct Test_SelectionEnter : emp::web::BaseTest {
D3::Selection svg_selection;
D3::Selection enter_selection;
emp::vector<int32_t> data{1, 2, 4, 8, 16, 32, 64};
// uint32_t enter_func_id = 0;
uint32_t append_func_id = 0;


Test_SelectionEnter() {
EM_ASM({
$("#emp_test_container").append("<svg id='test_svg'></svg>");
});

svg_selection = D3::Select("#test_svg");

enter_selection = svg_selection.SelectAll("circle").Data(data).Enter();

append_func_id = emp::JSWrap(
[this]() {
enter_selection.Append("circle").SetAttr("class", "test_circle");
},
"AppendSel"
);
}

~Test_SelectionEnter() {
// emp::JSDelete(enter_func_id);
emp::JSDelete(append_func_id);
}

void Describe() override {

EM_ASM({
describe("calling enter on a data-bound selection", function() {
it("should have 7 things in it", function() {
chai.assert.equal(emp_d3.objects[$0]._groups[0].length, 7);
});
});
}, enter_selection.GetID());

EM_ASM({
describe("calling append on our enter selection", function() {
it("should put circles in the svg", function() {
emp.AppendSel();
chai.assert.equal($("#test_svg").children(".test_circle").length, 7);
});
});
});
}
};


emp::web::MochaTestRunner test_runner;

Expand All @@ -91,7 +144,8 @@ int main() {

D3::internal::get_emp_d3();

test_runner.AddTest<Test_Selection>("Selection");
test_runner.AddTest<Test_SelectionConstruction>("SelectionConstruction");
test_runner.AddTest<Test_SelectionEnter>("Selection::Enter");

test_runner.OnBeforeEachTest([]() {
ResetD3Context();
Expand Down

0 comments on commit 06249e2

Please sign in to comment.