Commit c0e152ad authored by S Anand's avatar S Anand
Browse files

Merge g1 0.6.0

parents 6a6cbe85 a348822b
Pipeline #46677 failed with stage
in 1 minute and 41 seconds
......@@ -5,6 +5,7 @@ test/tape.js
# Ignore WIP files
wip/
TODO
# Ignore node related items
node_modules/
......
# Change log
- `0.6.0`: 15 Apr 2018
- [sanddance](#sanddance) smoothly animates selections into pre-defined and custom layouts
- [$.formhandler](#formhandler) and [g1.datafilter](#datafilter) support namespaces
- `0.5.0`: 31 Mar 2018
- [$.formhandler](#formhandler) has a `link:` option that links clicks to URLs. @tejesh.papineni
- [$.highlight](#highlight) adds classes to targets based on any event from any trigger
......
......@@ -24,10 +24,6 @@ Interactions:
- [g1.url.update](#urlupdate) updates a URL's query parameters
- [highlight.min.js](dist/highlight.min.js): highlighting library
- [$.highlight](#highlight) toggles classes on elements when clicked or hover
- [search.min.js](dist/search.min.js): search-as-you-type library
- [$.search](#search)
- [datafilter.min.js](dist/datafilter.min.js): filtering data library
- [$.datafilter](#datafilter) filters the data based on the options
Data components:
......@@ -42,6 +38,8 @@ Utilities:
- [$.template](#template) renders lodash templates. Requires [lodash](https://lodash.com/)
- [event.min.js](dist/event.min.js): event library
- [$.dispatch](#dispatch) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
- [datafilter.min.js](dist/datafilter.min.js): filtering data library
- [g1.datafilter](#datafilter) filters the data based on the options
- [types.min.js](dist/types.min.js): type detection library
- [g1.types](#types) returns the data types of columns in a DataFrames
......@@ -72,16 +70,16 @@ link *updates* the current page URL instead of replacing it.
For example:
```html
<a class="urlfilter" href="city=NY"> Change ?city= to NY</a>
<a class="urlfilter" href="city=NY" data-mode="add"> Add ?city= to NY</a>
<a class="urlfilter" href="city=NY" data-mode="del"> Remove NY from ?city=</a>
<a class="urlfilter" href="city=NY" data-mode="toggle"> Toggle NY in ?city=</a>
<a class="urlfilter" href="city=NY" data-target="pushState">Change ?city= to NY using pushState</a>
<a class="urlfilter" href="city=NY" data-target="#"> Change location.hash, i.e. #?city= to NY</a>
<a class="urlfilter" href="city=NY" data-target="iframe"> Change iframe URL ?city= NY</a>
<a class="urlfilter" href="?city=NY"> Change ?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-mode="add"> Add ?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-mode="del"> Remove NY from ?city=</a>
<a class="urlfilter" href="?city=NY" data-mode="toggle"> Toggle NY in ?city=</a>
<a class="urlfilter" href="?city=NY" data-target="pushState">Change ?city= to NY using pushState</a>
<a class="urlfilter" href="?city=NY" data-target="#"> Change location.hash, i.e. #?city= to NY</a>
<a class="urlfilter" href="?city=NY" data-target="iframe"> Change iframe URL ?city= NY</a>
<iframe src="?country=US"></iframe>
<a class="urlfilter" href="city=NY" data-target=".block"> Use AJAX to load ?city=NY into .block</a>
<a class="urlfilter" href="?city=NY" data-target=".block"> Use AJAX to load ?city=NY into .block</a>
<div class="block" src="?country=US"></div>
<script>
$('body').urlfilter() // Activate all the .urlfilter elements above
......@@ -160,25 +158,37 @@ Highlight containers use these attributes:
- `target`: elements that match the `data-target=` selector
## $.search
Highlight elements by searching as you type.
## datafilter
Example:
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
### $.search attributes
`g1.datafilter(data, filters)` returns the filtered data based on the filters. While urlfilter on [$.formhandler](#formhandler) applies filtering on data server side, `datafilter` applies urlfilter on frontend loaded data.
`.search` triggers use these attributes:
For example:
- `data-toggle="search"`
```js
var data = [
{"ID": "1", "product": "Fan", "sales": "100", "city": "NY"},
{"ID": "2", "product": "Fan", "sales": "80", "city": "London"},
{"ID": "3", "product": "Fan", "sales": "120", "city": "NJ"},
{"ID": "4", "product": "Fan", "sales": "130", "city": "London"},
{"ID": "5", "product": "Light", "sales": "500", "city": "NY"},
{"ID": "5", "product": "Light", "sales": "100", "city": "London"}
]
## datafilter
g1.datafilter(data, {
'sales>': ['100'],
'city': ['London', 'NJ'],
'product': ['Fan']
})
// Returns [{"ID": "3", "product": "Fan", "sales": "120", "city": "NJ"}, {"ID": "4", "product": "Fan", "sales": "130", "city": "London"}]
```
`g1.datafiilter(data, filters)` returns the filtered data based on the filters. For example:
g1.datafilter with multiple datasets:
```js
var data = [
var data1 = [
{"ID": "1", "product": "Fan", "sales": "100", "city": "NY"},
{"ID": "2", "product": "Fan", "sales": "80", "city": "London"},
{"ID": "3", "product": "Fan", "sales": "120", "city": "NJ"},
......@@ -187,46 +197,62 @@ var data = [
{"ID": "5", "product": "Light", "sales": "100", "city": "London"}
]
g1.datafilter(data, [{col: 'sales', op: '>', val: 100},
{col: 'city', op: 'in', val: ['London', 'NY']},
{col: 'product', val: 'Fan'}])
var data2 = [
{"ID": "1", "city": "NY"},
{"ID": "2", "city": "London"},
{"ID": "3", "city": "NJ"},
{"ID": "4", "city": "London"},
{"ID": "5", "city": "NY"},
{"ID": "5", "city": "London"}
]
// Returns [{"ID": "3", "product": "Fan", "sales": "120", "city": "NJ"}, {"ID": "4", "product": "Fan", "sales": "130", "city": "London"}]
```
g1.datafilter(data, {
'datsetname2:city': ['London', 'NJ'],
'sales>~': [100],
'datsetname1:product': ['Fan']
}, 'datsetname1'))
// ignores datsetname2:city: ['London', 'NJ']
// Returns [{"ID": "3", "product": "Fan", "sales": "120", "city": "NJ"}, {"ID": "4", "product": "Fan", "sales": "130", "city": "London"}, {"ID": "1", "product": "Fan", "sales": "100", "city": "NY"}]
var data2 = [
{"ID": "1", "city": "NY"},
{"ID": "2", "city": "London"},
{"ID": "3", "city": "NJ"},
{"ID": "4", "city": "London"},
{"ID": "5", "city": "NY"},
{"ID": "5", "city": "London"}
]
## datafilter options
g1.datafilter(data2, {
'datsetname2:city': ['London', 'NJ'],
'sales>~': [100],
'datsetname1:product': ['Fan']
}, 'datsetname2'))
datafilter() contains three parameters:
// ignores datsetname1:product: ['Fan']
- data: a list of objects
- filters: a list of objects, that will contains the below keys:
- col: column to be filtered.
- op: operator to be applied for filteration. default: `=`
- val: value of the selected column
- options: a dictionary that contains the below keys:
- limit: result is limited to. default: `1000`
- offset: filtering data should start from. default: `0`
- sort: a list of objects, that will contains the below keys:
- column: column to be sorted
- order: asc or desc. default: `asc`
- columns: a list of objects, that will contains the below keys:
- allow: a list of column names to be returned in the filtered data
- not: a list of column names to be skiped in the filtered data
// Return [
// {"ID": "2", "city": "London"},
// {"ID": "3", "city": "NJ"},
// {"ID": "4", "city": "London"},
// {"ID": "5", "city": "London"}
// ]
```
Rules:
## datafilter options
- the key `op` may contains any one of the below values:
- `=`
- `!=`
- `>`
- `<`
- `>=`
- `<=`
- `~`
- `!~`
- `in`
datafilter() contains three parameters:
- `data`: a list of objects
- `filters`: [formhandler filters][formhandler-filters] extracted using
`g1.url.parse(url).searchList`. This converts `?city=London&sales>=1000` to
this filters object: `{'city': ['London'], 'sales>': ['1000']}`
- `namespace`: (optional) If `namespace` is not given, all filters are applied
on the dataset. If `namespace` is given, only filters that begin with
`<namespace>:` or that have no `:` are applied
[formhandler-filters]: https://learn.gramener.com/guide/formhandler/#formhandler-filters
## $.formhandler
......@@ -505,6 +531,217 @@ $.get('topojson-file.json')
```
## sanddance
Transitions elements with flexible timing controls.
### sanddance options
It accepts 2 parameters
1. `attrs`: changes any HTML / SVG attributes of the selection (e.g. `fill`, etc)
- `fill`, `stroke` for color
- `x`, `y`, `cx`, `cy`, `transform`, etc for position (but these are better controlled via the options)
- etc.
2. `options`: defines layout options (e.g. `x`, `y`, `duration`, `speed`, etc)
- `x`: transition the elements to the x position
- `y`: transition the elements to the y position
- `speed`: transition at constant speed in pixels per second
- `duration`: transition time in milliseconds (overrides `speed`)
- `delay`: transition delay in milliseconds (default is 0)
- `easing`: transition easing in d3-ease method (default is d3.easeLinear)
- `filter`: filter the selection based on a column value (ex: age > 30)
Examples:
```js
// smoothly changes the fill color to blue in 100 ms
var turn_to_blue = g1.sanddance({ fill: 'blue' }, { duration: 100 })
selection.call(turn_to_blue)
// g1.sanddance moves elements to the x, y specified in 100ms
var move = g1.sanddance({}, { x: 200, y: 200, duration: 100 })
selection.call(move)
// transitions at 1000 pixels/sec.
var move_at_constant_speed = g1.sanddance({}, {x: 300, y: 300, speed: 1000})
selection.call(move_at_constant_speed)
// transitions in 100ms duration.
var move_in_duration = g1.sanddance({}, {x: 400, y: 400, duration: 100})
selection.call(move_in_duration)
// transitions after 100ms delay.
var move_after_delay = g1.sanddance({}, {x: 300, y: 300, delay: 100})
selection.call(move_after_delay)
// transitions at ease of d3.easeBounce
var move_easing = g1.sanddance({}, {x: 300, y: 300, duration: 100, easing: d3.easeBounce})
selection.call(move_easing)
// filters the selection
var selection_filtered = g1.sanddance({fill: 'red'}, {duration: 100, filter: function(d) { return d.age > 30}})
selection.call(selection_filtered)
```
### sanddance.chain
To apply a sequence of sanddances one after another, use `g1.sanddance.chain`. For example:
```js
// chain sanddances. First, fill everything red, then move x to 200 and y to 100
selection.call(
g1.sanddance.chain(
g1.sanddance({fill: 'red'}, {duration: 100}),
g1.sanddance({}, {x: 200, y: 100, duration: 100})
)
)
```
### sanddance layouts
#### 1. grid
Returns a sanddance that moves elements into a grid. Usage:
```js
// Lay out the elements in a grid
selection.call(
g1.sanddance({}, { // Create a layout based on the data array
layout: 'grid', // lay out as a grid
width: 400, // with width 400
height: 300, // and height 300
data: data, // using the specified data
sort: 'age', // sorted by the 'age' column
ascending: false, // in descending order
duration: 100 // in 100ms
})
)
```
Options:
- `data`: an array that has the data that is used to lay out the elements
- `width`: width of the grid in SVG units
- `height`: height of the grid in SVG units
- `sort`: column name or `function(d, i)` to sort by
- `ascending`: `false` reverses the sort order (default: `true`)
Values can be specified as a scale. Example: `fill:` can be specified as below.
```js
fill: {
metric: 'age',
scheme: 'RdYlGn'
}
// or
fill: {
metric: 'age', // same as function(d) { return d.age }
scale: 'linear',
domain: [0, 50, 100],
range: ['red', 'yellow', 'green'],
}
```
Scales are dictionaries with the following keys:
- `metric:` can be a string column name or a `function(d, i)` that returns a value for each item in the data
- `scheme:` d3 chromatic color scheme to interpolate to (e.g. `'RdYlGn'`)
- `scale:` d3 scale to use. Defaults to `'linear'`
- `range`: a list that contains the scale's range
- `domain`: a list that contains the scale's domain (defaults to the extent of the metric)
```js
// fill as scale config with linear scale
selection.call(
g1.sanddance({
fill: {
metric: function(d) { return d.age },
scale: 'linear',
domain: [0, 100],
range: ['red', 'blue'],
}
}, {
layout: 'grid',
data: data,
width: 400,
height: 300,
duration: 100
})
)
// fill as scale config with scheme
selection.call(
g1.sanddance({
fill: {
metric: function(d) { return d.age },
scheme: 'RdYlGn'
}
}, {
layout: 'grid',
data: data,
width: 400,
height: 300,
duration: 100
})
)
```
#### 2. hexpack
Returns a sanddance that moves elements into a hexpack. Usage:
```js
// Lay out the elements in a hexpack
selection.call(
g1.sanddance({}, { // Create a layout based on the data array
layout: 'hexpack', // lay out as hexpack
width: 400, // with width 400
height: 300, // and height 300
data: data, // using the specified data
sort: 'age', // sorted by the 'age' column
ascending: false, // in descending order
duration: 100 // in 100ms
})
)
```
The values can also be specified as a scale config and the options definitions are same as grid.
### sanddance methods
`sanddance.update(new_attrs, new_options)` returns a new sanddance that updates
the old `attrs` with `new_attrs` and the old `options` with `new_options`.
```js
// The two lines below are the same:
g1.sanddance({ stroke: 'blue' }).update({ fill: 'red' })
g1.sanddance({ stroke: 'blue', fill: 'red' })
// Update the options
g1.sanddance({}, { delay: 100 }).update({}, { duration: 100 }),
g1.sanddance({}, { duration: 100, delay: 100 })
```
### sanddance events
These events are triggered by the sanddance object.
- `init` when the sanddance is initialized
- `start` when the first transition begins (after delay)
- `end` when all transitions are complete
All events set the d3 selection as `this`. For example:
```js
// sanddance events
selection.call(
g1.sanddance({ fill: 'red' })
.on('init.log', function() { console.log('init', this) })
.on('start.log', function() { console.log('start', this) })
.on('end.log', function() { console.log('end', this) })
)
```
## url.parse
`g1.url` provides URL manipulation utilities.
......@@ -588,8 +825,8 @@ url.join(another_url, {query: false, hash: false})
For example:
```js
g1.url.parse('/').join('/?x=1#y=1', {hash: false}).toString() == '/?x=1';
g1.url.parse('/').join('/?x=1#y=1', {query: false}).toString() == '/#y=1';
g1.url.parse('/').join('/?x=1#y=1', {hash: false}).toString() == '/?x=1'
g1.url.parse('/').join('/?x=1#y=1', {query: false}).toString() == '/#y=1'
```
......
export { version } from './src/package.js'
export { datafilter } from './src/datafilter.js'
export { version } from './src/package.js'
export { sanddance } from './src/sanddance.js'
export { version } from './src/package.js'
export { types } from './src/types.js'
export { url } from './index-urlfilter.js'
export { datafilter } from './src/datafilter.js'
export { scale } from './src/scale.js'
export { datafilter } from './index-datafilter.js'
export { sanddance } from './src/sanddance.js'
import './index-highlight.js'
import './index-template.js'
import './index-formhandler.js'
......
This diff is collapsed.
{
"name": "g1",
"version": "0.5.0",
"version": "0.6.0",
"description": "Gramex 1.x interaction library",
"license": "UNLICENSED",
"author": "S Anand <s.anand@gramener.com>",
......
......@@ -12,6 +12,11 @@ export default [
plugins: [htmlparts('src/formhandler.template.html'), uglify()],
output: { file: "dist/g1.min.js", format: "umd", name: "g1" }
},
{
input: "index-datafilter",
plugins: [uglify()],
output: { file: "dist/datafilter.min.js", format: "umd", name: "g1" }
},
{
input: "index-urlfilter",
plugins: [uglify()],
......@@ -42,10 +47,14 @@ export default [
plugins: [uglify()],
output: { file: "dist/leaflet.min.js", format: "umd", name: "g1" }
},
{
input: "index-sanddance",
plugins: [uglify()],
output: { file: "dist/sanddance.min.js", format: "umd", name: "g1" }
},
{
input: "index-scale",
plugins: [uglify()],
output: { file: "dist/scale.min.js", format: "umd", name: "g1" }
}
]
var operators = {
'=': function(value, compare_with) { return value == compare_with },
'!=': function(value, compare_with) { return value != compare_with },
'>': function(value, compare_with) { return value > compare_with },
'<': function(value, compare_with) { return value < compare_with },
'>=': function(value, compare_with) { return value >= compare_with },
'<=': function(value, compare_with) { return value <= compare_with },
'in': function(value, compare_with) { return (compare_with.indexOf(value) != -1) },
'~': function(value, compare_with) { return (compare_with.indexOf(value) != -1) },
'!~': function(value, compare_with) { return (compare_with.indexOf(value) == -1) }
import { namespace } from './namespace_util.js'
function isEqual(value, compare_with, criteria_satisfied) {
// to handle: ( ...Shape!&... ) or ( ...&Shape&... )
if (!value) {
return criteria_satisfied ? (compare_with == null) : (compare_with != null)
}
return value.indexOf(compare_with) != -1 ? !criteria_satisfied : criteria_satisfied
}
var sorting = {
'string': function(value, compare_with) {
value = value.toUpperCase()
compare_with = compare_with.toUpperCase()
if (value < compare_with) {
return -1
}
if (value > compare_with) {
return 1
}
return 0
function greater_than(value, compare_with, include_equals) {
if ((isNaN(compare_with) && Date.parse(compare_with))) {
compare_with = Date.parse(compare_with)
value = Date.parse(value)
}
return include_equals ? (compare_with >= value) : (compare_with > value)
}
var operators = {
'=': function (value, compare_with) {
return isEqual(value, compare_with, false)
},
'!': function (value, compare_with) {
return isEqual(value, compare_with, true)
},
'>': function (value, compare_with) {
return greater_than(value, compare_with, false)
},
'<': function (value, compare_with) {
return greater_than(compare_with, value, false)
},
'>~': function (value, compare_with) {
return greater_than(value, compare_with, true)
},
'<~': function (value, compare_with) {
return greater_than(compare_with, value, true)
},
'~': function (value, compare_with) {
return isEqual(compare_with, value[0], false)
},
'number': function(value, compare_with) { return value - compare_with }
'!~': function (value, compare_with) {
return isEqual(compare_with, value[0], true)
}
}
var sorting = {
'string': function (value, compare_with, order) {
if (!order) order = 'asc'
// swap if 'desc'
if (order == 'desc')
value = [compare_with, compare_with = value][0]
export function datafilter(data, filters, options) {
filters = filters || []
return value.localeCompare(compare_with)
},
'number': function (value, compare_with, order) {
if (!order) order = 'asc'
// swap if 'desc'
if (order == 'desc')
value = [compare_with, compare_with = value][0]
options = options || {}
options.limit = options.limit || 1000
options.offset = options.offset || 0
return value - compare_with
}
}
options.sort = options.sort || []
function clone_pluck(source, include_keys, exclude_keys) {
if (include_keys.length == 0) include_keys = Object.keys(source)
var new_obj = {}
include_keys.forEach(function (key) {
if (exclude_keys.indexOf(key) < 0) new_obj[key] = source[key]
})
return new_obj
}
options.columns = options.columns || {}
options.columns.allow = options.columns.allow || []
options.columns.not = options.columns.not || []
var result_count = 0
var result = []
export function datafilter(data, filters, dataset_name) {
filters = filters || []
var result = data
var operator, value
for(var index = options.offset; index < data.length; index++) {
var criteria_statisfied = true
var row = data[index]
// url namespace sanitize
filters = namespace(filters, dataset_name)