Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
9b548ca
Small fixes
wch Apr 24, 2013
56e4666
set default date range for sunspots
May 14, 2013
9fa2891
update weblinks
May 14, 2013
144040e
fix typo in title
alexbbrown Sep 6, 2013
e7f7f4c
default to Airquality while debugging HACK
alexbbrown Sep 6, 2013
4bdbe00
add layers to README
alexbbrown Sep 6, 2013
d78960b
HACK sample layers plot airquality
alexbbrown Sep 6, 2013
61cd54a
don't forceTable in renderG3plot - we always do it manually, and laye…
alexbbrown Sep 6, 2013
fcbb682
rename table to data in message (may change this later)
alexbbrown Sep 6, 2013
436045c
move pluralise to d3functional. should probably be in a structure ut…
alexbbrown Sep 6, 2013
d7ce58d
parse layer aesthetics in g3message
alexbbrown Sep 6, 2013
11f6804
simplify margin calculations by creating a hasAesthetic routine
alexbbrown Sep 6, 2013
2da5bcf
add layer notes as notes.txt
alexbbrown Sep 6, 2013
67f44dd
only close errors for this g3plot - not all on page.
alexbbrown Sep 6, 2013
2f7459d
move hasAethetic into aestheticutils (for now)
alexbbrown Sep 6, 2013
c57ebe9
added setupLayerData, still integrating
alexbbrown Sep 6, 2013
fdb5a25
magic array object to object array convertor
alexbbrown Sep 6, 2013
6895aa1
typo
alexbbrown Sep 7, 2013
d15861f
more notes
alexbbrown Sep 7, 2013
9dbc284
fix plot layer spec in airquality example
alexbbrown Sep 7, 2013
d0ed151
adjust processLayer to bind layer to each aesthetic data item
alexbbrown Sep 7, 2013
958e0c3
simplify setupLayerData to become doLayerStats - and after that pull …
alexbbrown Sep 7, 2013
0e134f1
rename plotdatautils figuredatautils
alexbbrown Sep 7, 2013
cbf1861
save objectArraysToArrayObjects function in g3functional
alexbbrown Sep 7, 2013
db53bcb
thread layerdata through subfigure
alexbbrown Sep 7, 2013
f3b3a99
fix javascript filename issue
alexbbrown Sep 9, 2013
ea97b56
typo
alexbbrown Sep 9, 2013
be6679a
facetData only generates an extent when given an aesthetic (e.g. x fo…
alexbbrown Sep 9, 2013
95926b1
calculate cellFacets and layerFacets early
alexbbrown Sep 9, 2013
acfe93f
nearly there - added layer elements, fixing up geoms to be compatible.
alexbbrown Sep 9, 2013
490fab5
test case show only points (not voronoi) HACK
alexbbrown Sep 9, 2013
714dc9a
make sure geoms are preserved as arrays. This needs to be in g3widge…
alexbbrown Sep 10, 2013
8a60d1a
scale accessor needs to reach up an additional parent to skip the 'la…
alexbbrown Sep 10, 2013
4692868
Something odd happened to KEY function - not just in my new hack
alexbbrown Sep 10, 2013
3923a00
Make scaleColor work and only respond to Color aesthetic (no fills an…
alexbbrown Sep 10, 2013
2353c81
reorder stat application to avoid xCluster application blowing away t…
alexbbrown Sep 10, 2013
9bb8102
fix bug in asethetics that assumed 2 layers
alexbbrown Sep 10, 2013
77b7b5d
promote old plot spec to layered plot spec
alexbbrown Sep 10, 2013
ed344cf
DX always included in scale calculation
alexbbrown Sep 10, 2013
fc6ba33
use list instead of I(c in geom spec. needs fixing somewhere else.
alexbbrown Sep 10, 2013
dc418d7
fix x+dx calculation in two locations (should unify)
alexbbrown Sep 10, 2013
ebce9c5
fix color scale generation
alexbbrown Sep 10, 2013
968cfca
TODOs in legend for color / layer / filter fixes HACK
alexbbrown Sep 10, 2013
fe6b022
fix bug in domain discovery for "unit" data
alexbbrown Sep 10, 2013
e93f305
fix xcluster generation bugs - select from layer[0]. This is somewha…
alexbbrown Sep 10, 2013
4ccd576
probably pointbar bug
alexbbrown Sep 10, 2013
625f925
Fix position barstack by making it a layer property
alexbbrown Sep 10, 2013
84e221a
handle and upgrade 'reports'
alexbbrown Sep 10, 2013
84a19aa
Fix explicit color scale (capitalise)
alexbbrown Sep 10, 2013
f17faa7
filter notes
alexbbrown Sep 10, 2013
96fdbce
sample Color override
alexbbrown Sep 10, 2013
3d57ebc
Fix label legends (but only for first layer)
alexbbrown Sep 10, 2013
574e154
start to fix filters by accepting filter information frmo first layer…
alexbbrown Sep 10, 2013
139c547
fix voronoi
alexbbrown Sep 11, 2013
ccf81f3
restore original airquality for voronoi test.
alexbbrown Sep 11, 2013
75e3f73
more todos
alexbbrown Sep 11, 2013
a57786f
airquality model layer
alexbbrown Sep 11, 2013
7578c66
add layer class layer_name_<layer_name>. Might help with filtering l…
alexbbrown Sep 11, 2013
17e192f
enable grid at layer level, draw only last specified grid. There is …
alexbbrown Sep 11, 2013
ed92709
remove layer_name_<layer_name> from facet - it breaks transitions and…
alexbbrown Sep 11, 2013
e7d80b9
comments and todos
alexbbrown Sep 12, 2013
7115421
put timestamp hack back in - to convert to dates.
alexbbrown Sep 12, 2013
0ff8832
note that the ozone dataset is interesting
alexbbrown Sep 12, 2013
1707ccd
add master_layer to control faceting, filtering and brushing.
alexbbrown Sep 12, 2013
793c15a
extract (seemingly unused) negate
alexbbrown Sep 12, 2013
5f312dd
Fix html nested table header duplication bug
alexbbrown Sep 12, 2013
a8ae0ce
Fix zoom+brush by attaching zoom behaviour to zoom_layer
alexbbrown Sep 12, 2013
150b758
disable touch style zooming
alexbbrown Sep 12, 2013
0ea3f34
add notes
alexbbrown Sep 12, 2013
b6197f8
add notes to readme
alexbbrown Sep 12, 2013
c1ca758
Fix line filtering by cloning line nest key property into line nest C…
alexbbrown Sep 12, 2013
57206a4
Fix onClick crash in geom loop
alexbbrown Sep 12, 2013
38a0145
todo bug notes
alexbbrown Sep 12, 2013
6808840
allow date format y labels. Date (no time) only so far, will fix
alexbbrown Aug 27, 2013
d365491
added area geom
alexbbrown Aug 27, 2013
17413bd
enable date formatted yfacet
alexbbrown Aug 27, 2013
9baacb9
added facetMargin option to dimensions to control y facet
alexbbrown Aug 27, 2013
5af4f10
Set area fill to Fill instead of Color aesthetic
alexbbrown Aug 27, 2013
aaf2cbb
facetMargin fix configuration to work with 0
alexbbrown Aug 29, 2013
bfbedbd
only poistion brushes if brushes enabled
alexbbrown Aug 29, 2013
9795816
hack to move y label inside graph area. make this configurable
alexbbrown Aug 30, 2013
f9d4a1e
fix another location where brush existence is assumed wrongly
alexbbrown Aug 30, 2013
b1d74ce
HACK use local javascript
alexbbrown Sep 12, 2013
dd36cc6
include g3widget in javascript
alexbbrown Sep 12, 2013
af8c794
Area fixes after merge
alexbbrown Sep 12, 2013
b51df63
includes fix lost includes
alexbbrown Sep 12, 2013
26fefda
includes fix lost includes
alexbbrown Sep 12, 2013
61624bb
use external includes
alexbbrown Sep 12, 2013
fbc196d
Airpassengers is demo of area plot
alexbbrown Sep 12, 2013
18de184
Fix line 'animations' - color is a transition not a permanent state
alexbbrown Sep 13, 2013
fa7bdbe
comment
alexbbrown Sep 13, 2013
f2e4274
Add Range property to plotspec to allow actual colors used to be over…
alexbbrown Sep 13, 2013
cc65819
don't color lines from the group aesthetic - only the color aesthetic
alexbbrown Sep 13, 2013
cb08be8
more todos
alexbbrown Sep 13, 2013
b276784
allow color scales even without color aes - let's us color the defaults
alexbbrown Sep 16, 2013
656b6cc
added rudimentary x grid support (need to be able to deactivate)
alexbbrown Sep 16, 2013
d85101a
don't color Group if color is missing
alexbbrown Sep 16, 2013
b7ea7b6
remove grid flicker (turn off crispedges)
alexbbrown Sep 16, 2013
0daf7d8
y grid also enabled
alexbbrown Sep 16, 2013
646581c
adjust legend margin
alexbbrown Sep 17, 2013
b906c2a
Replace remaining uses of Fill with Color
alexbbrown Oct 8, 2013
a006d40
allow graph 'limits' to absolutely clip ranges. works well for Y, le…
alexbbrown Oct 8, 2013
a41e5d2
change x and y limits to use limits.x.all and limits.y.all to allow s…
alexbbrown Oct 8, 2013
9c3ddbb
Add untested yfacet free limits
alexbbrown Oct 8, 2013
fabcaca
move to external javascript
alexbbrown Nov 8, 2013
22c5671
fixes for demo app including allowing atomic keys and disabling voron…
Nov 8, 2013
5747fe8
Merge local changes with demo changes
alexbbrown Nov 13, 2013
daa88bb
Visual fix for co-incident voronoi
alexbbrown Nov 13, 2013
d65ec93
updated README with getting started instructions for new apps, and a …
alexbbrown Nov 13, 2013
ce9ce8d
updated comments in touch zoom
alexbbrown Nov 13, 2013
ca31b83
Handle null in line or area as missing point
alexbbrown Nov 13, 2013
2cf9ffc
Add plotspec limits:{x: y:} to restrict data range even if wider data…
alexbbrown Nov 13, 2013
68770e9
found odd code that needs attention later
alexbbrown Nov 13, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 84 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ A demo framework for Shiny + D3 including ggplot-like aesthetic mapping and geom

Written by Alex B Brown at Intel Corp, 2012-2013

Core idea: Pick a dataset, then describe how that data is mapped into a graph, using the handy-dangy ggplot2 like format.
Core idea: Pick a dataset, then describe how that data is mapped into a graph, using the handy-dandy ggplot2 like format.

Includes interactive features such as click and drag.

Add Shiny inputs to the app to control graph filtering and more.

Now supports layers

Licence
-------

Expand All @@ -19,6 +21,10 @@ See the files LICENCE and NOTICE for licence terms.
Usage as a demo app
-------------------

Recommended packages to install:

shiny, plyr, httr, hmisc, reshape2, stringr, lubridate, ggplot2 (plus dependencies),

To run g3plot in demo mode,

1) start R in this directory.
Expand All @@ -36,16 +42,63 @@ Look in plot.R and add some functions for other data sets. If that gets old, go

If *that* gets old, start to create new graph types in javascript, or fix the html table logic.

Usage as your own app
---------------------
Writing a new g3plot application
--------------------------------

You can start with this super simple application and extend it:

mkdir myproject
cd myproject
git clone <g3plot repo> g3plot

Checkout g3plot as a subdirectory of your Shiny Application and add the line
Create the file `server.R` with the contents:

```
require(shiny)
source("g3plot/shiny_extend_g3.R")
addResourcePath("js",tools:::file_path_as_absolute("./g3plot/js"))
shinyServer(function(input,output,session){
output$testplot = renderG3Plot(function() {
dataSet = data.frame(x=1:10,y=1:10)
list(type="plot",
table=forceTableVector(dataSet),
structure=list(sX="x",sY="y"),
aesthetic=list(X="sX",Y="sY"),
geom="point")})
})
```

Create the file `ui.R` with the contents:

```
require(shiny)
source("g3plot/shiny_extend_g3.R")
shinyUI(
pageWithSidebar(div(),div(),
div(includeHTML("g3plot/g3widget.html"),
svgOutput("testplot")))
)
```

Updating an existing shiny application to use g3plot
----------------------------------------------------

Checkout g3plot as a subdirectory of your Shiny Application

```
cd myproject
git clone <g3plot repo> g3plot
```

and add the line

```
addResourcePath("js",tools:::file_path_as_absolute("./g3plot/js"))
```

To your server.R.
To your `server.R`.

Then follow some of the examples in server.R and ui.R and friends to add
Then follow some of the examples in the demo `server.R` and `ui.R` and friends to add
javascript plots to your Shiny Application.

Note that you can still test the demo app by using
Expand All @@ -57,12 +110,24 @@ Geoms

Currently supports:

point
bar
point_bar
point_range_bar
line
voronoi (doesn't display but makes UI better)
name | required aesthetics | optional aesthetics | axis | description
----------------|---------------------|---------------------|--------|---
point | X,Y | Color | cont** | a small round point
line | X,Y,Group | Group,Color | cont | a line for each Group (or Color)
area | X,Y,Group | Group,Color | cont | an area underneath the line for each group (stacking?)
bar | X,Y | Color | ordinal| a bar starting at 0, can be stacked
point_bar | X,Y | Color | ordinal| instead of a whole bar, just the tip
point_range_bar | X,DX,Y | Color | cont | like point bar but each can have a unique width (DX)
voronoi | X,Y,Label | | cont | Use with points to extend click/hover halo around point

** *cont* is `linear` or `log`

Other aesthetics supported by most geoms include:

* Label - What appears when you hover. Default is cloned from XCluster or X aesthetic.
* XCluster - Compound X axis - see examples for more details.
* YFacet - Facet plot into rows with separate synchronised Y axes.
* Key - improve animation by giving each node a unique key.

Layout Structure
----------------
Expand All @@ -74,13 +139,15 @@ Common Name | Entity | Arity | Class | File | Message Part
Document | HTML | 1 | | UI.R | - | the web page
report | DIV* | n | g3plotMultiPlex | g3widget.js | Array of arrays | the shiny output
section | DIV | n | pane | g3report.js | Array of list(name=?) | a single formatted d3 object - one of "plot" "list" or other text
figure+data | DIV | 1 | plot | g3plot.js | ditto | A combination of a drawing region with linked html table
figure+grid | DIV | 1 | plot | g3plot.js | ditto | A combination of a drawing region with linked html table
figure | SVG | 1 | d3svg | g3plot.js | ditto | A single drawing region with any contents
subfigure | G | n | subplot | g3subfigure.js | List(name=?) | container for a plot with distinct axes, data, legends
plot | G | 1 | plot | g3plot.js | ditto | the bit inside the axes
y facet | G | J | facet | g3plot.js | aesthetic(YFacet=?) | the Jth horizontal slice with a personal clone of the Y scale
x facet | G | K | y facet | g3plot.js | aesthetic(XCluster=?)| the Kth vertical slice of the Jth horizontal
layer | G | L | layer | g3plot.js | Array of list(name=?) | a single formatted d3 object - one of "layer"
geom | ? | many | dot/bar/... | g3geom.js | aesthetic(geom=?) | an actual drawing component
grid | TABLE | 1 | XX?table | ? | like layer but grid | An html table

TODO
----
Expand All @@ -98,16 +165,16 @@ Fun improvements:

* Standardised way to add dynamic tooltips
* Standardised way to hover highlight nodes
* Improved click dropzones (voronoi?)
* Improved click dropzones (voronoi?) (now implemented)
* click drag on axes to scale (near ends) or pan (in middle)
* Mouseover cursor with tooltip coordinates of intersecting line / point and selection like brushes.

Some ideas to develop the product
Some ideas to develop the tool

* Sparklines - providing a very simple 3-layer structure without faceting
* Layers - multiple geoms on top of each other (another layer!)
* Layers - multiple geoms on top of each other (another layer!) (now implemented)
* g3autoplot - looks at data and does something sensible, then tells you how to repeat / customise it


Constraints
-----------

Expand Down
2 changes: 1 addition & 1 deletion dynamicUI.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ output$dataSetControls <- renderUI({

if (is.null(hashParts())) return(NULL)
hashDataSelected <- hashParts()$dataSet
selectedDataSets <- NULL
selectedDataSets <- "airquality"
if (!is.null(hashDataSelected)) {
selectedDataSets <- as.numeric(strsplit(hashDataSelected,"\\|")[[1]])
selectedDataSets <- intersect(selectedDataSets,groupedDataSets)
Expand Down
10 changes: 7 additions & 3 deletions g3widget.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<!-- g3widget.html is included in the HTML uploaded by UI.R.
It handles included javascript and extended css -->
<!--<script src="external/d3.v3.js"></script>-->
<!--<script src="external/underscore-min.js"></script>-->
<script src="http://d3js.org/d3.v3.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<!-- the original version -->
Expand All @@ -13,14 +15,15 @@
<script src="js/g3report.js" type="text/javascript"></script>
<script src="js/g3htmlPanes.js" type="text/javascript"></script>
<script src="js/g3geoms.js" type="text/javascript"></script>
<script src="js/g3widget.js" type="text/javascript"></script>
<script src="js/g3brush.js" type="text/javascript"></script>
<script src="js/g3events.js" type="text/javascript"></script>
<script src="js/g3math.js" type="text/javascript"></script>
<script src="js/g3stats.js" type="text/javascript"></script>
<script src="js/g3legends.js" type="text/javascript"></script>
<script src="js/g3xcluster.js" type="text/javascript"></script>
<script src="js/g3functional.js" type="text/javascript"></script>
<script src="js/g3plotDataUtils.js" type="text/javascript"></script>
<script src="js/g3figureDataUtils.js" type="text/javascript"></script>
<script src="js/g3patterns.js" type="text/javascript"></script>
<script src="js/d3extend.js" type="text/javascript"></script>
<script src="js/g3URL.js" type="text/javascript"></script>
Expand All @@ -34,10 +37,11 @@
.axis path,
.axis line {
fill: none;
stroke: black;
stroke: lightgrey;
stroke-width: 1px;
shape-rendering: crispEdges; // warning: zoom out in chrome disappears 1px crisp lines.
// shape-rendering: crispEdges; // warning: zoom out in chrome disappears 1px crisp lines.
}

rect.haxis {
// stroke: white;
// stroke-width: 1;
Expand Down
24 changes: 22 additions & 2 deletions js/aestheticUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
return recordWalker.other(
// for each string S in structure, pull out the indexed element
// of the array named S in data.
recordWalker.indexedMemberOf(message.table,index))(structure);
recordWalker.indexedMemberOf(message.data,index))(structure);
}
// the number of records:
var recordCount = _.values(message.table)[0].length
var recordCount = _.values(message.data)[0].length
// use the walker to map the structure onto the data
var records = d3.range(0,recordCount).map(buildRecord)

Expand Down Expand Up @@ -63,5 +63,25 @@
return _.isUndefined(filterWalker.walk(aesFilterSpec,record))
}
}

// utilities - shouldn't be here really since it's about the concrete plan containing aesthetics.

// returns length of (first?) valid aesthetic or undefined if none
exports.hasAesthetic = function hasAesthetic(plan,aesthetic) {
// look in global aesthetic structure first
var aesthetics = []
if (plan.metaData && plan.aestheticStructure && plan.metaData.aestheticStructure[aesthetic]) {
aesthetics.push(plan.metaData.aestheticStructure[aesthetic])
}
// look in local aesthetic next
_.map(plan.layers,function(l){
if (l.metaData.aestheticStructure[aesthetic]) {
aesthetics.push(l.metaData.aestheticStructure[aesthetic])
}
})
// return the length of the olength of the first aesthetic (or 1 for atomics or objects)
return _.isObject(aesthetics[0]) ?
_.keys(aesthetics[0]).length : !_.isUndefined(aesthetics[0])
}

})(typeof exports === 'undefined'? this['aestheticUtils']={}: exports);
39 changes: 25 additions & 14 deletions js/g3figure.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,21 @@
g3figure.filter.addWidget(d.subfigure.filterHandle());
})

if(plans.length == 0 || plans[plans.length-1].data.message.grid == null){
// find first layer with grid
var grid_layers = _.chain(plans).pluck("layers").flatten(true).filter(function(layer){return !_.isUndefined(layer.data.message.grid)}).value()


if(grid_layers.length==0){
// need to remove the table
d3.select(el).select(".d3Table").select("table").remove()
// and turn off the filters. um, why?
//exports.filter.clear()
} else {
// It should mean - draw a table for each subFigure that wants one. But I only
// know how to draw one table at the moment so only the last one that wants one
// It should mean - draw a table for each subfigure and layer that wants one. But I only
// know how to draw one table at the moment so only the last layer that wants one
// right now it does 'if the last plan wants a table' do it.
if(plans[plans.length-1].data.message.grid != null) {
exports.filter.addWidget(g3figure.table(d3.select(el).select(".d3Table"),plans[plans.length-1]));
}
var grid_layer=_.last(grid_layers)
exports.filter.addWidget(g3figure.table(d3.select(el).select(".d3Table"),grid_layer));
}
}

Expand All @@ -194,20 +197,25 @@
})
subFigure.exit().remove();


// calculate the inner sizes for the subFigure
// adjust figure margins where aesthetics require guides
subFigure
.each(function(plan,i){
var xAxisHeight = 0
var xClusterAxisHeight = 0
if (plan.metaData.aestheticStructure.X) {
var legendWidth = 20
if (aestheticUtils.hasAesthetic(plan,"X")) {
xAxisHeight += 25
}
if (plan.metaData.aestheticStructure.XCluster) {
if (aestheticUtils.hasAesthetic(plan,"XCluster")) {
// 1+length because there's always an outer cluster to select all
xClusterAxisHeight = (1+_.keys(plan.metaData.aestheticStructure.XCluster).length) * 20
xClusterAxisHeight = (1+aestheticUtils.hasAesthetic(plan,"XCluster")) * 20
}
if (aestheticUtils.hasAesthetic(plan,"Color") || aestheticUtils.hasAesthetic(plan,"Fill")) {
// 1+length because there's always an outer cluster to select all
legendWidth += 100
}

// TODO: enable user to disable showing guides

var s=d3.select(this);
// calculate the inner sizes. Should go in d3subfigure since
Expand All @@ -218,7 +226,7 @@
var dimensions = plan.dimensions
dimensions.margin = {
top: (widgets.size=="history")?0:20,
right: 160,
right: legendWidth,
bottom: (widgets.size=="mini")?10:
(widgets.size=="history")?25:(xAxisHeight + xClusterAxisHeight),
xcluster: xClusterAxisHeight,
Expand Down Expand Up @@ -256,8 +264,11 @@
}

// filter is a per-plot (possibly global) filter collection that
// sends filter messages amongst graphs. Should rebuild to use
// d3.dispatch (but what about new graphs - how do they get filters?)
// sends filter messages amongst graphs.

// TODO: Should rebuild to use either:
// * d3.dispatch (but what about new graphs - how do they get filters?)
// * just using css selectors to find filterable things, then invoke update on them?
exports.filter = function() {
exports.filter.widgets = []
exports.filter.unfilter = function() {
Expand Down
3 changes: 3 additions & 0 deletions js/g3plotDataUtils.js → js/g3figureDataUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
})
.entries(dataToFacet)

if (_.isUndefined(coordAesthetic)) return dnest;

// not convinced the code below is used. not needed for cell facets, anyway
var coordAccessor = function(d){return d[coordAesthetic]}

// now decorate the nested entries with yscale
Expand Down
9 changes: 9 additions & 0 deletions js/g3functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
return function repeatedly() { return v[i=(1+i)%l] }
}

exports.negate = function negate(f) { return function(x){ return !f(x) } }

// using an arbitrary accessor as the next pointer, return the list thus formed from the input node
exports.followChain=function(accessor){
var followChainAccessor = function(node) {
Expand All @@ -19,6 +21,13 @@
}
return followChainAccessor
}

// other utilities
exports.pluralise = function(x){return typeof(x)==="undefined"?[]:_.isArray(x)?x:[x]}

// convert an array of objects to an object of arrays [{a:1},{a:2}] => {a:[1,2]}
// unused so far - just handy
var twoargs=function(f){return function(x,y){return f(x,y)}}
exports.objectArraysToArrayObjects=function(d){return (function(k){return _.object(k,_.map(k,function(x){return _.pluck(d,x)}))})(_.chain(d).map(_.keys).reduce(twoargs(_.unique)).value())}

})(typeof exports === 'undefined'? this['g3functional']={}: exports);
Loading