Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Tables.jl interface and ag-grid #6

Merged
merged 12 commits into from
Feb 19, 2019
7 changes: 4 additions & 3 deletions REQUIRE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
julia 0.6
julia 0.7
WebIO
JSExpr
JuliaDB
DataValues
Tables
JSON
Observables
2 changes: 2 additions & 0 deletions deps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ag-grid
build.log
13 changes: 13 additions & 0 deletions deps/build.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
isdir(joinpath(@__DIR__, "ag-grid")) || mkdir(joinpath(@__DIR__, "ag-grid"))

ag_grid_base = joinpath(@__DIR__, "ag-grid", "ag-grid.js")
isfile(ag_grid_base) || download("https://unpkg.com/ag-grid-community/dist/ag-grid-community.min.noStyle.js", ag_grid_base)

ag_grid_base_style = joinpath(@__DIR__, "ag-grid", "ag-grid.css")
isfile(ag_grid_base_style) || download("https://unpkg.com/ag-grid-community/dist/styles/ag-grid.css", ag_grid_base_style)

ag_grid_light = joinpath(@__DIR__, "ag-grid", "ag-grid-light.css")
isfile(ag_grid_light) || download("https://unpkg.com/ag-grid-community/dist/styles/ag-theme-balham.css", ag_grid_light)

ag_grid_dark = joinpath(@__DIR__, "ag-grid", "ag-grid-dark.css")
isfile(ag_grid_dark) || download("https://unpkg.com/ag-grid-community/dist/styles/ag-theme-balham-dark.css", ag_grid_dark)
210 changes: 121 additions & 89 deletions src/TableView.jl
Original file line number Diff line number Diff line change
@@ -1,119 +1,151 @@
module TableView

using WebIO
using JSExpr
using JuliaDB
using DataValues
using Tables
using WebIO, JSExpr, JSON, Dates, UUIDs
using Observables: @map

import JuliaDB: DNDSparse, DNextTable, NextTable
export showtable

function JuliaDB.subtable(t::DNextTable, r)
table(collect(rows(t)[r]), pkey=t.pkey)
end
const ag_grid_imports = []

showna(xs) = xs
function showna(xs::AbstractArray{T}) where {T<:DataValue}
map(xs) do x
isnull(x) ? "NA" : get(x)
function __init__()
empty!(ag_grid_imports)
for f in ["ag-grid.js", "ag-grid.css", "ag-grid-light.css", "ag-grid-dark.css"]
push!(ag_grid_imports, normpath(joinpath(@__DIR__, "..", "deps", "ag-grid", f)))
end
end

function showna(xs::Columns)
rows(map(showna, columns(xs)))
end

function showtable(t::Union{DNextTable, NextTable}; rows=1:100, colopts=Dict(), kwargs...)
w = Scope(imports=["https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.js",
"https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.css"])
function showtable(table; dark = false, height = 500)
if !Tables.istable(typeof(table))
throw(ArgumentError("Argument is not a table."))
end

trunc_rows = max(1, first(rows)):min(length(t), last(rows))
subt = JuliaDB.subtable(t, trunc_rows)
tablelength = Base.IteratorSize(table) == Base.HasLength() ? length(Tables.rows(table)) : nothing

rows = Tables.rows(table)
schema = Tables.schema(table)
if schema === nothing
types = []
for (i, c) in enumerate(Tables.eachcolumn(first(rows)))
push!(types, typeof(c))
end
names = collect(propertynames(first(rows)))
else
names = schema.names
types = schema.types
end
w = Scope(imports = ag_grid_imports)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shashi Is there a way to directly style the div introduced by the Scope? This results in something like

<div class="webio-mountpoint interactbulma" data-webio-mountpoint="13462147745702835718">
  <div class="webio-scope" data-webio-scope-id="scope-4acc84b9-6adc-4a65-8534-e785737a7c2d">
    <div id="grid" class="ag-theme-balham" style="height: 100%; min-height: 200px; width: 100%;">

right now, but I'd also like to give the Scope (and the mountpoint, I guess) a style to allow piping through the parent's width and height.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have that right now, but the way to do it would be allow s(style=Dict(:foo => bar)) to work where s is a Scope (just like it works for Node)...


coldefs = [(
headerName = n,
headerTooltip = types[i],
field = n,
type = types[i] <: Union{Missing, T where T <: Number} ? "numericColumn" : nothing,
filter = types[i] <: Union{Missing, T where T <: Dates.Date} ? "agDateColumnFilter" :
types[i] <: Union{Missing, T where T <: Number} ? "agNumberColumnFilter" : nothing
) for (i, n) in enumerate(names)]

id = string("grid-", string(uuid1())[1:8])
w.dom = dom"div"(className = "ag-theme-balham$(dark ? "-dark" : "")",
style = Dict("width" => "100%",
"height" => "$(height)px"),
id = id)

tablelength === nothing || tablelength > 10_000 ? _showtable_async!(w, names, types, rows, coldefs, tablelength, dark, id) :
_showtable_sync!(w, names, types, rows, coldefs, tablelength, dark, id)

headers = colnames(subt)
cols = [merge(Dict(:data=>n), get(colopts, n, Dict())) for n in headers]
w
end

function _showtable_sync!(w, names, types, rows, coldefs, tablelength, dark, id)
options = Dict(
:data => showna(collect(JuliaDB.rows(subt))),
:colHeaders => headers,
:modifyColWidth => @js(w -> w > 300 ? 300 : w),
:modifyRowHeight => @js(h -> h > 60 ? 50 : h),
:manualColumnResize => true,
:manualRowResize => true,
:columns => cols,
:width => 800,
:height => 400,
:rowData => JSONText(table2json(rows, names, types)),
:columnDefs => coldefs,
:enableSorting => true,
:enableFilter => true,
:enableColResize => true,
:multiSortKey => "ctrl",
)
if (length(t.pkey) > 0 && t.pkey == [1:length(t.pkey);])
options[:fixedColumnsLeft] = length(t.pkey)
end

merge!(options, Dict(kwargs))

handler = @js function (Handsontable)
@var sizefix = document.createElement("style");
sizefix.textContent = """
.htCore td {
white-space:nowrap
}
"""
this.dom.appendChild(sizefix)
this.hot = @new Handsontable(this.dom, $options);
handler = @js function (agGrid)
@var gridOptions = $options
@var el = document.getElementById($id)
this.table = @new agGrid.Grid(el, gridOptions)
gridOptions.columnApi.autoSizeColumns($names)
end
onimport(w, handler)
w.dom = dom"div"()
w
end

function showtable(t::Union{DNDSparse, NDSparse}; rows=1:100, colopts=Dict(), kwargs...)
w = Scope(imports=["https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.js",
"https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.css"])
data = Observable{Any}(w, "data", [])

trunc_rows = max(1, first(rows)):min(length(t), last(rows))

ks = keys(t)[trunc_rows]
vs = values(t)[trunc_rows]

if !isa(keys(t), Columns)
ks = collect(ks)
vs = collect(vs)
function _showtable_async!(w, names, types, rows, coldefs, tablelength, dark, id)
rowparams = Observable(w, "rowparams", Dict("startRow" => 1,
"endRow" => 100,
"successCallback" => @js v -> nothing))
requestedrows = Observable(w, "requestedrows", JSONText("{}"))
on(rowparams) do x
requestedrows[] = JSONText(table2json(rows, names, types, requested = [x["startRow"], x["endRow"]]))
end

subt = NDSparse(showna(ks), showna(vs))

headers = colnames(subt)
cols = [merge(Dict(:data=>n), get(colopts, n, Dict())) for n in headers]
onjs(requestedrows, @js function (val)
($rowparams[]).successCallback(val, $(tablelength))
end)

options = Dict(
:data => JuliaDB.rows(subt),
:colHeaders => headers,
:fixedColumnsLeft => ndims(t),
:modifyColWidth => @js(w -> w > 300 ? 300 : w),
:modifyRowHeight => @js(h -> h > 60 ? 50 : h),
:manualColumnResize => true,
:manualRowResize => true,
:columns => cols,
:width => 800,
:height => 400,
:columnDefs => coldefs,
:enableSorting => true,
:enableFilter => true,
:maxConcurrentDatasourceRequests => 1,
:cacheBlockSize => 1000,
:maxBlocksInCache => 100,
:enableColResize => true,
:multiSortKey => "ctrl",
:rowModelType => "infinite",
:datasource => Dict(
"getRows" =>
@js function (rowParams)
$rowparams[] = rowParams
end
,
"rowCount" => tablelength
)
)

merge!(options, Dict(kwargs))

handler = @js function (Handsontable)
@var sizefix = document.createElement("style");
sizefix.textContent = """
.htCore td {
white-space:nowrap
}
"""
this.dom.appendChild(sizefix)
this.hot = @new Handsontable(this.dom, $options);
handler = @js function (agGrid)
@var gridOptions = $options
@var el = document.getElementById($id)
this.table = @new agGrid.Grid(el, gridOptions)
gridOptions.columnApi.autoSizeColumns($names)
end
onimport(w, handler)
w.dom = dom"div"()
w
end

showtable(t; kwargs...) = showtable(table(t); kwargs...)
# directly write JSON instead of allocating temporary dicts etc
function table2json(rows, names, types; requested = nothing)
io = IOBuffer()
print(io, '[')
for (i, row) in enumerate(rows)
if requested == nothing || first(requested) <= i <= last(requested)
print(io, '{')
i = 1
for col in Tables.eachcolumn(row)
JSON.print(io, names[i])
i += 1
print(io, ':')
if col isa Number
JSON.print(io, col)
else
JSON.print(io, sprint(print, col))
end
print(io, ',')
end
skip(io, -1)
print(io, '}')
print(io, ',')
end
end
skip(io, -1)
print(io, ']')

end # module
String(take!(io))
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unfortunate thing is that I can't figure out how to transfer the data with WebIO without an additional JSON.parse on the javascript side. Any ideas on how to circumvent that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a type in JSON.jl called JSONString which was introduced for this purpose

end
11 changes: 8 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using TableView
using Base.Test
using Test, WebIO

# write your own tests here
@test 1 == 2
@test isfile(joinpath(@__DIR__, "..", "deps", "ag-grid", "ag-grid.js"))

nttable = [
(a = 2.0, b = 3),
(a = 3.0, b = 12)
]
@test showtable(nttable) isa WebIO.Scope