I'd like to try FFI with JS.
For that purpose, I'd like to play with crossfilter.
A minimal crossfilter example looks like example.js:
var livingThings = crossfilter([
// Fact data.
{ name: "Rusty", type: "human", legs: 2 },
{ name: "Alex", type: "human", legs: 2 },
{ name: "Lassie", type: "dog", legs: 4 },
{ name: "Spot", type: "dog", legs: 4 },
{ name: "Polly", type: "bird", legs: 2 },
{ name: "Fiona", type: "plant", legs: 0 }
]);
// How many living things are in my house?
var n = livingThings.groupAll().reduceCount().value();
console.log("There are " + n + " living things in my house.") // 6
With index.html:
<html>
<body>
<script type="text/javascript" src="crossfilter.min.js"></script>
<script type="text/javascript" src="example.js"></script>
</body>
</html>
I don't know how should I send the data to the JavaScript function. I tried something like the following for the FFI part:
import json
when not defined(js):
{.error: "This module only works on the JavaScript platform".}
type
Crossfilter = ref object
proc crossfilter*(a:JsonNode): Crossfilter {.importc:"crossfilter".}
proc groupAll*(a:Crossfilter):Crossfilter {.importc:"crossfilter.groupAll".}
proc reduceCount*(a:Crossfilter):Crossfilter {.importc:"crossfilter.reduceCount".}
proc value*(a:Crossfilter):int {.importc:"crossfilter.value".}
and the following for the Nim replacement code:
import crossfilter, json
let data = """
[
{ name: "Rusty", type: "human", legs: 2 },
{ name: "Alex", type: "human", legs: 2 },
{ name: "Lassie", type: "dog", legs: 4 },
{ name: "Spot", type: "dog", legs: 4 },
{ name: "Polly", type: "bird", legs: 2 },
{ name: "Fiona", type: "plant", legs: 0 }
]
"""
let livingThings = crossfilter(data.parseJson)
var n = livingThings.groupAll().reduceCount().value()
echo "There are " & $n & " living things in my house."
How should I send data to crossfilter?
It looks good. For the record, I did:
import jsffi
...
proc crossfilter*(a:seq[JsObject]): Crossfilter {.importc:"crossfilter".}
...
and
import crossfilter, jsffi
let data = @[
js{ name: "Rusty", type: "human", legs: 2 },
js{ name: "Alex", type: "human", legs: 2 },
js{ name: "Lassie", type: "dog", legs: 4 },
js{ name: "Spot", type: "dog", legs: 4 },
js{ name: "Polly", type: "bird", legs: 2 },
js{ name: "Fiona", type: "plant", legs: 0 }
]
I have another question for the other functions. Given that crossfilter is defined in the JS world like:
var crossfilter = {
add: add,
remove: removeData,
dimension: dimension,
groupAll: groupAll,
size: size,
all: all,
allFiltered: allFiltered,
onChange: onChange,
isElementFiltered: isElementFiltered
};
Should I do something like:
Crossfilter* {.importc.} = object of RootObj
add*: proc (this:JsObject)
remove*: proc (this:Crossfilter)
dimension*: proc (this:Crossfilter)
groupAll*: proc (this:Crossfilter)
...
or it should be something like what I was doing at the beginning?
proc groupAll*(a:Crossfilter):Crossfilter {.importc:"crossfilter.groupAll".}
Another example but with vega-lite:
import jsffi, strutils, json, jsconsole
proc vegaEmbed(id: cstring, vlSpec: JsObject) {.importc.}
proc parseJsonToJs*(json: cstring): JsObject {.importjs: "JSON.parse(#)".}
var vlSpec = """{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {"url": "/v1/data/circ"},
"width": "container",
"height": "container",
"mark": "bar",
"encoding": {
"y": {"field": "a", "type": "nominal"},
"x": {
"aggregate": "average",
"field": "b",
"type": "quantitative",
"axis": {
"title": "Average of b"
}
}
}
}"""
vegaEmbed("#vis", parseJsonToJS(vlSpec.cstring))
@juancarlospaco, gracias!
Finally I managed to get it working with:
import jsffi
when not defined(js):
{.error: "This module only works on the JavaScript platform".}
type
Crossfilter* = ref object
proc crossfilter*(a:openArray[JsObject]): Crossfilter {.importc:"crossfilter".}
proc groupAll*(this:Crossfilter):Crossfilter {.importcpp.}
proc reduceCount*(this:Crossfilter):Crossfilter {.importcpp.}
proc value*(this:Crossfilter):int {.importcpp.}
import crossfilter, jsffi
let data = @[
js{ name: "Rusty", type: "human", legs: 2 },
js{ name: "Alex", type: "human", legs: 2 },
js{ name: "Lassie", type: "dog", legs: 4 },
js{ name: "Spot", type: "dog", legs: 4 },
js{ name: "Polly", type: "bird", legs: 2 },
js{ name: "Fiona", type: "plant", legs: 0 }
]
let livingThings = crossfilter(data)
var n = livingThings.groupAll().reduceCount().value()
echo "There are " & $n & " living things in my house."
It depends how you want to design, you can make it more safe, or not.
You can see an example on the FetchOptions of jsfetch, has newfetchOptions and unsafeNewfetchOptions https://nim-lang.github.io/Nim/jsfetch.html#unsafeNewFetchOptions%2Ccstring%2Ccstring%2Ccstring%2Ccstring%2Ccstring%2Ccstring%2Cbool
One more question: how can I "import" a .js file from nim (kind like dynlib, when doing FFI with C).
I mean something like:
when defined(nodejs):
import crossfilter from './crossfilter.min.js' # <--- This is the Javascript that I want to replace
This is in order to do:
$ nim js -d:nodejs example
$ node example.js
import jsffi
when not defined(js):
{.error: "This module only works on the JavaScript platform".}
type
Tabulator* = ref object
proc tabulator*(element:string, options:JsObject): Tabulator {. importcpp: "new Tabulator(@)" .}
proc setData*(this:Tabulator, a:openArray[JsObject]):JsObject {.importcpp:"#.setData(@)", discardable.}
include karax / prelude, tables, jsffi, json, tabulator
var tabledata = @[
js{ id:1, name:"Oli Bob", age:"12", col:"red", dob:"" },
js{ id:2, name:"Mary May", age:"1", col:"blue", dob:"14/05/1982" },
js{ id:3, name:"Christine Lobowski", age:"42", col:"green", dob:"22/05/1982" },
js{ id:4, name:"Brendon Philips", age:"125", col:"orange", dob:"01/08/1980" },
js{ id:5, name:"Margret Marmajuke", age:"16", col:"yellow", dob:"31/01/1999" },
]
var tmp = parseJson(
"""
{
height:205, // set height of table (in CSS or here), this enables the Virtual DOM and improves render speed dramatically (can be any valid css height value)
data:tabledata, //assign data to table
layout:"fitColumns", //fit columns to width of table (optional)
columns:[ //Define Table Columns
{title:"Name", field:"name", width:150},
{title:"Age", field:"age", hozAlign:"left", formatter:"progress"},
{title:"Favourite Color", field:"col"},
{title:"Date Of Birth", field:"dob", sorter:"date", hozAlign:"center"},
],
rowClick:function(e, row){ //trigger an alert message when the row is clicked
alert("Row " + row.getData().id + " Clicked!!!!");
},
}
"""
)
proc createDom(): VNode =
result = buildHtml(tdiv):
h1(text "Hello Karax", class="title")
var table = tabulator("#example-table", tmp.toJs)
table.setData(tabledata)
setRenderer createDom
I am not sure anout how should I wrap setData. In principle, it should be easy, but I get the error:
Error: type mismatch: got <VNode, JsObject>
but expected one of:
proc add(parent, kid: VNode)
first type mismatch at position: 2
required type for kid: VNode
but expression 'setData(table, tabledata)' is of type: JsObject
19 other mismatching symbols have been suppressed; compile with --showAllMismatches:on to see them
expression: add(tmp_369114261, setData(table, tabledata))
That VNode seems related with Karax.Thanks. I found that I need to use cstring on the tabulator declaration.
I am also defining the options as follows:
var opts = newJsObject()
opts.height = 205
opts.layout = "fitColumns".cstring
#opts.columns = columns
But I am stuck with the columns.
In JS, it is defined like:
var columns = [ //Define Table Columns
{title:"Name", field:"name", sorter:"string", width:150},
{title:"Age", field:"age", sorter:"number", hozAlign:"left", formatter:"progress"},
{title:"Favourite Color", field:"col", sorter:"string", sortable:false},
{title:"Date Of Birth", field:"dob", sorter:"date", hozAlign:"center"}, ];
]
I tried several things:
var columns = @[
js{ title:"Name", field:"name", sorter:"string", width:150 },
js{ title:"Age", field:"age", sorter:"number", hozAlign:"left", formatter:"progress" },
js{ title:"Favourite Color", field:"col", sorter:"string", sortable:false },
js{ title:"Date Of Birth", field:"dob", sorter:"date", hozAlign:"center" },
]
...
opts.columns = columns
#opts.columns = columns.toJs
Also with the:
proc parseJsonToJs*(json: cstring): JsObject {.importjs: "JSON.parse(#)".}
How should I add the list to opts.columns?
Right now I provide the data as:
var tabledata = @[
js{ id:1, name:"Oli Bob".cstring, age:"12".cstring, col:"red".cstring, dob:"".cstring },
js{ id:2, name:"Mary May".cstring, age:"1".cstring, col:"blue".cstring, dob:"14/05/1982".cstring },
js{ id:3, name:"Christine Lobowski".cstring, age:"42".cstring, col:"green".cstring, dob:"22/05/1982".cstring },
js{ id:4, name:"Brendon Philips".cstring, age:"125".cstring, col:"orange".cstring, dob:"01/08/1980".cstring },
js{ id:5, name:"Margret Marmajuke".cstring, age:"16".cstring, col:"yellow".cstring, dob:"31/01/1999".cstring },
]
> Uses the js{...} macro and the .cstring for every string.
I can avoid it by means of:
var data = @[
( id:1, name:"Oli Bob", age: 12, col:"red", dob:""),
( id:2, name:"Mary May", age:1, col:"blue", dob:"14/05/1982" ),
( id:3, name:"Christine Lobowski", age:42, col:"green", dob:"22/05/1982" ),
( id:4, name:"Brendon Philips", age:125, col:"orange", dob:"01/08/1980" ),
( id:5, name:"Margret Marmajuke", age:16, col:"yellow", dob:"31/01/1999" ),
]
proc set(this:Tabulator, data:seq[tuple[id:int,name:string, age:int, col:string, dob:string]]) =
var tmp = newSeq[JsObject]()
for i in data:
var obj = newJsObject()
obj.id = i.id
obj.name = i.name.cstring
obj.age = i.age
obj.col = i.col.cstring
obj.dob = i.dob.cstring
tmp &= obj
this.setData(tmp)
How could I convert set into a more generic function that admits any generic sequence of tuples? Is it a macro my only alternative? Are you aware if such a macro already exists?
Something like this:
import jsffi
var console {.importc, nodecl.}: JsObject
func set(t: tuple): JsObject =
result = newJsObject()
for name, value in t.fieldPairs:
when value is string:
result[name] = value.cstring
else:
result[name] = value
when isMainModule:
console.log test.set((id: 1, name: "hello this is my name", data: @[1, 2, 3, 4]))
console.log test.set((anotherId: 1, anotherName: @[34, 35], anotherData: @[1, 2, 3, 4], someMoreData: "this is some random string that doesn't mean much"))
Thanks a lot. This is much easier than the macro that I was trying to implement.
In order to handle seq[tuple] I just added:
func set(tt: seq[tuple]): seq[JsObject] =
result = newSeq[JsObject]()
for t in tt:
var tmp = newJsObject()
for name, value in t.fieldPairs:
when value is string:
tmp[name] = value.cstring
else:
tmp[name] = value
result &= tmp