slamdata / purescript-jtable

A Purescript table renderer capable of displaying multidimensional, heterogeneous JSON data
14 stars 11 forks source link

Implement purescript-jtable #10

Closed brainrake closed 9 years ago

brainrake commented 9 years ago

Here's my implementation of purescript-jtable. It has all the high-level features in the requirements. Some things are implemented differently though, and I hope you will accept my reasoning below.

I recommend to Try it with different renderers and example data (including the "Comprehensive" example) right away, look at example usage and then read the analysis below.

Types, header

The label hierarchy is unified based on its shape. It becomes a hierarchy of nodes with width. Primitives have width 1, tuples more. The concrete types of the primitives are irrelevant, only the width is used. Arrays are not represented, they are unified into the most general shape hierarchy. Thus it is ensured that all data present in the Json can be rendered sensibly.

Contrast this to the spec, which says "Classify every leaf node using JSemantic [...], based on the most common type of semantic detected in the data". The problems with this step include that a statistical method is suggested to determine the type of data, with no hints as to how to handle divergent leaves. Also, leaf node semantic type, and even distinguishing between primitive json types is irrelevant to the header generation, where only tuple width and label hierarchy matters. In fact, JSemantic is a different issue, independent of JTable (but which combines nicely), as discussed below.

Tuples

There were some undefined aspects of handling tuples. Here's how I chose to resolve them:

Homogenous arrays of equal sizes are rendered vertically, except when their size equals 2, in which case they are rendered horizontally. This is somewhat arbitrary, but usefult for geographic coordinates.

Heterogenous arrays of different sizes are rendered horizontally, with the maximum tuple size becoming the column width

Lists of objects with dijunct keys will be displayed in one row (treating them as tuples of objects).

TableStyle.th

Another difference is: JPath is an array of strings. Arrays (which could have different nestings) are not represented in the header hierarchy, so a [String] suffices for the path instead of the unwieldier JCursor.

type JPath = [String]

The header rendering function gets a JPath as an argument.

TableStyle.th :: JPath -> Markup

The default implementation is equivalent to

noStyle.th = \p -> th $ text $ Data.Array.Unsafe.last p

Where th and text are from Smolder and the path is guaranteed to have at least one element.

Column Ordering

The ColumnOrdering type has been simplified, inOrdering is a value. Easy to roll your own.

type ColumnOrdering = JPath -> JPath -> Ordering 
inOrdering    :: ColumnOrdering
alphaOrdering :: ColumnOrdering

TableStyle.td

JTable does not depend on JSemantic. In fact, I recommend releasing JSemantic as a separate library, since it is orthogonal to JTable and separate from Argonaut (although it uses the JsonPrim type). However, for now I have inluded it here.

Accordingly, td receives a JsonPrim argument that is easy to use.

TableStyle.td :: JCursor -> JsonPrim -> Markup

JSemantic

Data constructors of the type JSemantic have parameters and carry their data with them, converted to a useful type.

data JSemantic = Integral   Number
               | Fractional Number
               | Date       Date.Date
               | DateTime   Date.Date
               | Time       Date.Date
               | Interval   Date.Date Date.Date
               | Text       String
               | Bool       Boolean
               | Percent    Number
               | Currency   Number
               | NA

Although String might be a better choice for Currency and Percent, to avoid loss of precision.

And it's easy to combine JTable and JSemantic:

exampleRendererSemantic = renderJTable defJTableOpts {
    style = noStyle { td = \c j -> case toSemantic j of
      Currency n -> td ! className "money" $ text $ show n
  }}

Rendering differently based on path is also easy, dispatching on the JCursor argument, using operations from Argonaut.

Also,

There are unit tests for some edge cases that I fixed along the way, and StrongCheck tests are run as well.

The code is not exceptionally fast or clean and simple, but it's not slow or too complex either. It gets the job done.

I'm sure the examples, the readme, and the above will convince you that the implementation provides a straightforward, intuitive, and correct conceptual mapping between any Json schema and the resulting table, has features equivalent to the requirements, with the differences backed by sound analysis.

Let me know if changes or additions are needed.

jdegoes commented 9 years ago

Wow, I will review on Monday. Can you squash all these commits down so we can get a clean commit history?