Skip to content

Commit

Permalink
Merge branch 'collections'
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Oct 20, 2015
2 parents f5757d2 + f3393cd commit 13ec581
Show file tree
Hide file tree
Showing 9 changed files with 828 additions and 24 deletions.
112 changes: 106 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Utilities for array manipulation: ordering, summarizing, searching, etc.

When using D3—and with data visualization in general—you tend to do a lot of array manipulation. That’s because D3’s canonical representation of data is an array. Some common forms of array manipulation include taking a contiguous slice (subset) of an array, filtering an array using a predicate function, and mapping an array to a parallel set of values using a transform function. Before looking at the set of utilities that D3 provides for arrays, you should familiarize yourself with the powerful [array methods built-in to JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype).
When using D3—and with data in general—you tend to do a lot of array manipulation. Some common forms of array manipulation include taking a contiguous slice (subset) of an array, filtering an array using a predicate function, and mapping an array to a parallel set of values using a transform function. Before looking at the set of utilities that D3 provides for arrays, you should familiarize yourself with the powerful [array methods built-in to JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype).

JavaScript includes **mutator methods** that modify the array:

Expand Down Expand Up @@ -208,9 +208,11 @@ pairs([1, 2, 3, 4]); // returns [[1, 2], [2, 3], [3, 4]]

If the specified array has fewer than two elements, returns the empty array.

### Associative Arrays
### Collections

Another common data type in JavaScript is the associative array, or more simply the object, which has a set of named properties. In Java this is referred to as a map, and in Python, a dictionary. JavaScript provides a standard mechanism for iterating over the keys (or property names) in an associative array: the [for…in loop](https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in). However, note that the iteration order is undefined. D3 provides several operators for converting associative arrays to standard indexed arrays.
Another common data type in JavaScript is the associative array, or more simply the object, which has a set of named properties. Java refers to this as a *map*, and Python a *dictionary*. JavaScript provides a standard mechanism for iterating over the keys (or property names) in an associative array: the [for…in loop](https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in). However, note that the iteration order is undefined. D3 provides several operators for converting associative arrays to standard arrays with numeric indexes.

A word of caution: it is tempting to use plain objects as maps, but this causes [unexpected behavior](http://www.devthought.com/2012/01/18/an-object-is-not-a-hash/) when built-in property names are used as keys, such as `object["__proto__"] = 42` and `"hasOwnProperty" in object`. (ES6 introduces Map and Set collections which avoid this problem, but browser support is limited.) If you cannot guarantee that map keys and set values will be safe, you should use [map](#map) and [set](#set) instead of plain objects.

<a name="keys" href="#keys">#</a> <b>keys</b>(<i>object</i>)

Expand All @@ -228,6 +230,99 @@ Returns an array containing the property keys and values of the specified object
entries({foo: 42, bar: true}); // [{key: "foo", value: 42}, {key: "bar", value: true}]
```

<a name="map" href="#map">#</a> <b>map</b>([<i>object</i>[, <i>key</i>]])

Constructs a new map. If *object* is specified, copies all enumerable properties from the specified object into this map. The specified object may also be an array or another map. An optional *key* function may be specified to compute the key for each value in the array. For example:

```js
var m = map([{name: "foo"}, {name: "bar"}], function(d) { return d.name; });
m.get("foo"); // {"name": "foo"}
m.get("bar"); // {"name": "bar"}
m.get("baz"); // undefined
```

Note: unlike ES6 Map, D3’s map coerces keys to strings.

See also [nest](#nest).

<a name="map_has" href="#map_has">#</a> map.<b>has</b>(<i>key</i>)

Returns true if and only if this map has an entry for the specified *key* string. Note: the value may be `null` or `undefined`.

<a name="map_get" href="#map_get">#</a> map.<b>get</b>(<i>key</i>)

Returns the value for the specified *key* string. If the map does not have an entry for the specified *key*, returns `undefined`.

<a name="map_set" href="#map_set">#</a> map.<b>set</b>(<i>key</i>, <i>value</i>)

Sets the *value* for the specified *key* string; returns the new *value*. If the map previously had an entry for the same *key* string, the old entry is replaced with the new value.

<a name="map_remove" href="#map_remove">#</a> map.<b>remove</b>(<i>key</i>)

If the map has an entry for the specified *key* string, removes the entry and returns true. Otherwise, this method does nothing and returns false.

<a name="map_keys" href="#map_keys">#</a> map.<b>keys</b>()

Returns an array of string keys for every entry in this map. The order of the returned keys is arbitrary.

<a name="map_values" href="#map_values">#</a> map.<b>values</b>()

Returns an array of values for every entry in this map. The order of the returned values is arbitrary.

<a name="map_entries" href="#map_entries">#</a> map.<b>entries</b>()

Returns an array of key-value objects for each entry in this map. The order of the returned entries is arbitrary. Each entry’s key is a string, but the value has arbitrary type.

<a name="map_forEach" href="#map_forEach">#</a> map.<b>forEach</b>(<i>function</i>)

Calls the specified *function* for each entry in this map, passing the entry's key and value as two arguments. The `this` context of the *function* is this map. Returns undefined. The iteration order is arbitrary.

<a name="map_empty" href="#map_empty">#</a> map.<b>empty</b>()

Returns true if and only if this map has zero entries.

<a name="map_size" href="#map_size">#</a> map.<b>size</b>()

Returns the number of entries in this map.

<a name="set" href="#set">#</a> <b>set</b>([<i>array</i>])

Constructs a new set. If *array* is specified, adds the given *array* of string values to the returned set.

Note: unlike ES6 Set, D3’s set coerces values to strings.

<a name="set_has" href="#set_has">#</a> set.<b>has</b>(<i>value</i>)

Returns true if and only if this set has an entry for the specified *value* string.

<a name="set_add" href="#set_add">#</a> set.<b>add</b>(<i>value</i>)

Adds the specified *value* string to this set. Returns *value*.

<a name="set_remove" href="#set_remove">#</a> set.<b>remove</b>(<i>value</i>)

If the set contains the specified *value* string, removes it and returns true. Otherwise, this method does nothing and returns false.

<a name="set_values" href="#set_values">#</a> set.<b>values</b>()

Returns an array of the string values in this set. The order of the returned values is arbitrary. Can be used as a convenient way of computing the unique values for a set of strings. For example:

```js
set(["foo", "bar", "foo", "baz"]).values(); // "foo", "bar", "baz"
```

<a name="set_forEach" href="#set_forEach">#</a> set.<b>forEach</b>(<i>function</i>)

Calls the specified *function* for each value in this set, passing the value as an argument. The `this` context of the *function* is this set. Returns undefined. The iteration order is arbitrary.

<a name="set_empty" href="#set_empty">#</a> set.<b>empty</b>()

Returns true if and only if this set has zero values.

<a name="set_size" href="#set_size">#</a> set.<b>size</b>()

Returns the number of values in this set.

### Nest

Nesting allows elements in an array to be grouped into a hierarchical tree structure; think of it like the GROUP BY operator in SQL, except you can have multiple levels of grouping, and the resulting output is a tree rather than a flat table. The levels in the tree are specified by key functions. The leaf nodes of the tree can be sorted by value, while the internal nodes can be sorted by key. An optional rollup function will collapse the elements in each leaf node using a summary function. The nest operator (the object returned by [nest](#nest)) is reusable, and does not retain any references to the data that is nested.
Expand Down Expand Up @@ -302,7 +397,13 @@ Specifies a rollup *function* to be applied on each group of leaf elements. The

<a name="nest_map" href="#nest_map">#</a> nest.<b>map</b>(<i>array</i>)

Applies the nest operator to the specified *array*, returning a map. Each entry in the returned associative array corresponds to a distinct key value returned by the first key function. The entry value depends on the number of registered key functions: if there is an additional key, the value is another nested associative array; otherwise, the value is the array of elements filtered from the input *array* that have the given key value.
Applies the nest operator to the specified *array*, returning a nested [map](#map). Each entry in the returned map corresponds to a distinct key value returned by the first key function. The entry value depends on the number of registered key functions: if there is an additional key, the value is another map; otherwise, the value is the array of elements filtered from the input *array* that have the given key value.

<a name="nest_object" href="#nest_object">#</a> nest.<b>object</b>(<i>array</i>)

Applies the nest operator to the specified *array*, returning a nested object. Each entry in the returned associative array corresponds to a distinct key value returned by the first key function. The entry value depends on the number of registered key functions: if there is an additional key, the value is another associative array; otherwise, the value is the array of elements filtered from the input *array* that have the given key value.

Note: this method is unsafe if any of the keys conflict with built-in JavaScript properties, such as `__proto__`. If you cannot guarantee that the keys will be safe, you should use [nest.map](#nest_map) instead.

<a name="nest_entries" href="#nest_entries">#</a> nest.<b>entries</b>(<i>array</i>)

Expand All @@ -311,5 +412,4 @@ Applies the nest operator to the specified *array*, returning an array of key-va
## Changes from D3 3.x:

* The [range](#range) method now returns the empty array for infinite ranges, rather than throwing an error.
* The map and set classes have been removed. Please use ES6 Map and Set instead.
* The [nest.map](#nest_map) method now always returns an ES6 Map.
* The [nest.map](#nest_map) method now always returns a [map](#map); use [nest.object](#nest_object) to return a plain object instead.
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import deviation from "./src/deviation";
import entries from "./src/entries";
import extent from "./src/extent";
import keys from "./src/keys";
import map from "./src/map";
import max from "./src/max";
import mean from "./src/mean";
import median from "./src/median";
Expand All @@ -16,6 +17,7 @@ import pairs from "./src/pairs";
import permute from "./src/permute";
import quantile from "./src/quantile";
import range from "./src/range";
import set from "./src/set";
import shuffle from "./src/shuffle";
import sum from "./src/sum";
import transpose from "./src/transpose";
Expand All @@ -34,6 +36,7 @@ export {
entries,
extent,
keys,
map,
max,
mean,
median,
Expand All @@ -44,6 +47,7 @@ export {
permute,
quantile,
range,
set,
shuffle,
sum,
transpose,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-arrays",
"version": "0.0.4",
"version": "0.1.0",
"description": "Array manipulation, ordering, searching, summarizing, etc.",
"keywords": [
"d3",
Expand Down
70 changes: 70 additions & 0 deletions src/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export var prefix = "$";

function Map() {}

Map.prototype = map.prototype = {
has: function(key) {
return (prefix + key) in this;
},
get: function(key) {
return this[prefix + key];
},
set: function(key, value) {
return this[prefix + key] = value;
},
remove: function(key) {
var property = prefix + key;
return property in this && delete this[property];
},
keys: function() {
var keys = [];
for (var property in this) if (property[0] === prefix) keys.push(property.slice(1));
return keys;
},
values: function() {
var values = [];
for (var property in this) if (property[0] === prefix) values.push(this[property]);
return values;
},
entries: function() {
var entries = [];
for (var property in this) if (property[0] === prefix) entries.push({key: property.slice(1), value: this[property]});
return entries;
},
size: function() {
var size = 0;
for (var property in this) if (property[0] === prefix) ++size;
return size;
},
empty: function() {
for (var property in this) if (property[0] === prefix) return false;
return true;
},
forEach: function(f) {
for (var property in this) if (property[0] === prefix) f.call(this, property.slice(1), this[property]);
}
};

function map(object, f) {
var map = new Map;

// Copy constructor.
if (object instanceof Map) object.forEach(function(key, value) { map.set(key, value); });

// Index array by numeric index or specified key function.
else if (Array.isArray(object)) {
var i = -1,
n = object.length,
o;

if (arguments.length === 1) while (++i < n) map.set(i, object[i]);
else while (++i < n) map.set(f.call(object, o = object[i], i), o);
}

// Convert object to map.
else for (var key in object) map.set(key, object[key]);

return map;
}

export default map;
43 changes: 31 additions & 12 deletions src/nest.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import map from "./map";

export default function() {
var keys = [],
sortKeys = [],
sortValues,
rollup,
nest;

function map(array, depth) {
function apply(array, depth, createResult, setResult) {
if (depth >= keys.length) return rollup
? rollup.call(nest, array) : (sortValues
? array.sort(sortValues)
Expand All @@ -16,8 +18,9 @@ export default function() {
key = keys[depth++],
keyValue,
value,
valuesByKey = new Map,
values;
valuesByKey = map(),
values,
result = createResult();

while (++i < n) {
if (values = valuesByKey.get(keyValue = key(value = array[i]) + "")) {
Expand All @@ -27,22 +30,21 @@ export default function() {
}
}

valuesByKey.forEach(function(values, key) {
valuesByKey.set(key, map(values, depth));
valuesByKey.forEach(function(key, values) {
setResult(result, key, apply(values, depth, createResult, setResult));
});

return valuesByKey;
return result;
}

function entries(map, depth) {
if (depth >= keys.length) return map;

var array = new Array(map.size),
i = -1,
var array = [],
sortKey = sortKeys[depth++];

map.forEach(function(value, key) {
array[++i] = {key: key, values: entries(value, depth)};
map.forEach(function(key, value) {
array.push({key: key, values: entries(value, depth)});
});

return sortKey
Expand All @@ -51,11 +53,28 @@ export default function() {
}

return nest = {
map: function(array) { return map(array, 0); },
entries: function(array) { return entries(map(array, 0), 0); },
object: function(array) { return apply(array, 0, createObject, setObject); },
map: function(array) { return apply(array, 0, createMap, setMap); },
entries: function(array) { return entries(apply(array, 0, createMap, setMap), 0); },
key: function(d) { keys.push(d); return nest; },
sortKeys: function(order) { sortKeys[keys.length - 1] = order; return nest; },
sortValues: function(order) { sortValues = order; return nest; },
rollup: function(f) { rollup = f; return nest; }
};
};

function createObject() {
return {};
}

function setObject(object, key, value) {
object[key] = value;
}

function createMap() {
return map();
}

function setMap(map, key, value) {
map.set(key, value);
}
29 changes: 29 additions & 0 deletions src/set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {default as map, prefix} from "./map";

function Set() {}

var proto = map.prototype;

Set.prototype = set.prototype = {
has: proto.has,
add: function(value) {
value += "";
this[prefix + value] = true;
return value;
},
remove: proto.remove,
values: proto.keys,
size: proto.size,
empty: proto.empty,
forEach: function(f) {
for (var property in this) if (property[0] === prefix) f.call(this, property.slice(1));
}
};

function set(array) {
var set = new Set;
if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]);
return set;
}

export default set;
Loading

0 comments on commit 13ec581

Please sign in to comment.