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".}
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))
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
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
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 },
 ]
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