├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── LICENSE
├── README.md
├── bin
└── rollup.cjs
├── build
└── d3-jetpack.cjs
├── d3-index.js
├── essentials.js
├── index.js
├── package-lock.json
├── package.json
├── selection.js
├── src
├── append.js
├── appendMany.js
├── ascendingKey.js
├── at.js
├── attachTooltip.js
├── clamp.js
├── conventions.js
├── descendingKey.js
├── drawAxis.js
├── f.js
├── insert.js
├── interval.js
├── loadData.js
├── nestBy.js
├── parent.js
├── parseAttributes.js
├── polygonClip.js
├── round.js
├── selectAppend.js
├── st.js
├── timeout.js
├── timer.js
├── translate-selection.js
├── tspans.js
└── wordwrap.js
└── test
├── helpers
└── makeDocument.cjs
├── index.html
├── test-append.cjs
├── test-at.cjs
├── test-clamp.cjs
├── test-f.cjs
├── test-nestBy.cjs
├── test-parseAttributes.cjs
├── test-selectAppend.cjs
├── test-st.cjs
├── test-translate-selection.cjs
├── test-tspans.cjs
└── test-wordwrap.cjs
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on: ['push']
3 | jobs:
4 | lint-and-test:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: actions/setup-node@v2
9 | with:
10 | node-version: '14'
11 | - name: Install dependencies
12 | run: npm ci --no-optional
13 | - name: Run tests
14 | run: npm run test
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | test.html
5 | yarn.lock
6 | package.lock
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019, Gregor Aisch
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of the author nor the names of contributors may be used to
15 | endorse or promote products derived from this software without specific prior
16 | written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | d3-jetpack is a set of nifty convenience wrappers that speed up your daily work with d3.js
2 |
3 | [](http://myjetpack.tumblr.com/post/23725103159)
4 |
5 | (comic by [Tom Gauld](http://myjetpack.tumblr.com/]))
6 |
7 | ## Usage
8 |
9 | If you use NPM, `npm install d3-jetpack`. Otherwise, download the latest [d3v4+jetpack.js](https://raw.githubusercontent.com/gka/d3-jetpack/master/build/d3v4%2Bjetpack.js).
10 |
11 | Here's what's in the package:
12 |
13 | # selection.append(selector) [<>](https://github.com/gka/d3-jetpack/blob/master/src/append.js "Source")
14 |
15 | Modifies `append` so it adds classes and ids.
16 |
17 | ```js
18 | selection.append("div.my-class");
19 | selection.append("div.first-class.second-class");
20 | selection.append("div#someId");
21 | selection.append("div#someId.some-class");
22 | ```
23 |
24 | # selection.insert(selector) [<>](https://github.com/gka/d3-jetpack/blob/master/src/insert.js "Source")
25 |
26 | Works with insert, too:
27 |
28 | ```js
29 | selection.insert("div.my-class");
30 | ```
31 |
32 |
33 | # selection.appendMany(selector, array) [<>](https://github.com/gka/d3-jetpack/blob/master/src/appendMany.js "Source")
34 |
35 | Instead of making an empty selection, binding data to it, taking the enter selection and appending elements as separate steps:
36 |
37 | ```js
38 | selection.selectAll('div.my-class')
39 | .data(myArray)
40 | .enter()
41 | .append('div.my-class');
42 | ```
43 |
44 | use `appendMany`:
45 |
46 | ```js
47 | selection.appendMany('div.my-class', myArray);
48 | ```
49 |
50 | # selection.at(name[, value]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/at.js "Source")
51 |
52 | Works like d3v3's `.attr`. Passing an object to name sets multiple attributes, passing a string returns a single attribute and passing a string & second argument sets a single attribute.
53 |
54 | To avoid having to use quotes around attributes and styles with hyphens when using the object notation, camelCase keys are hyphenated. Instead of:
55 |
56 | ```js
57 | selection
58 | .attr('stroke-width', 10)
59 | .attr('text-anchor', 'end')
60 | .attr('font-weight', 600)
61 | ```
62 |
63 | or with [d3-selection-multi](https://github.com/d3/d3-selection-multi):
64 |
65 | ```js
66 | selection.attrs({'stroke-width': 10, 'text-anchor': 'end', 'font-weight': 600})
67 | ```
68 |
69 | you can write:
70 |
71 | ```js
72 | selection.at({fontSize: 10, textAnchor: 'end', fontWeight: 600})
73 | ```
74 |
75 | With syntax highlighting on, it is a little easier to see the difference between keys and values when everything isn't a string. Plus there's less typing!
76 |
77 | # selection.st(name[, value]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/st.js "Source")
78 |
79 | Like `at`, but for `style`. Additionally, when a number is passed to a style that requires a unit of measure, like `margin-top` or `font-size`, `px` is automatically appended. Instead of
80 |
81 | ```js
82 | selection
83 | .style('margin-top', height/2 + 'px')
84 | .style('font-size', '40px')
85 | .style('width', width - 80 + 'px')
86 | ```
87 |
88 | The `+ px`s can also be dropped:
89 |
90 | ```js
91 | selection.st({marginTop: height/2, fontSize: 40, width: width - 80})
92 | ```
93 |
94 | # d3.selectAppend(selector) [<>](https://github.com/gka/d3-jetpack/blob/master/src/selectAppend.js "Source")
95 |
96 | Selects the first element that matches the specified selector string or if no elements match the selector, it will append an element. This is often handy for elements which are required as part of the DOM hierachy, especially when making repeated calls to the same code. When appending it will also add id and classes, same as Jetpack's [append](#append)
97 |
98 | ```js
99 | d3.selectAppend('ul.fruits')
100 | .selectAll('li')
101 | .data(data)
102 | ```
103 |
104 | # d3.parent() [<>](https://github.com/gka/d3-jetpack/blob/master/src/parent.js "Source")
105 |
106 | Returns the parent of each element in the selection:
107 |
108 | ```js
109 | d3.selectAll('span')
110 | .style('color', 'red')
111 | .parent()
112 | .style('background', 'yellow')
113 | ```
114 |
115 | This might mess with the joined data and/or return duplicate elements. Usually better to save a variable, but sometimes useful when working with nested html.
116 |
117 | # selection.translate(xyPosition, [dim]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/translate.js "Source")
118 |
119 | How I hated writing ``.attr('transform', function(d) { return 'translate()'; })`` a thousand times...
120 |
121 | ```js
122 | svg.append('g').translate([margin.left, margin.top]);
123 | circle.translate(function(d) { return [x(d.date), y(d.value)]; });
124 | ```
125 |
126 | If you only want to set a *single* dimension you can tell translate by passing 0 (for x) or 1 (for y) as second argument:
127 |
128 | ```js
129 | x_ticks.translate(d3.f(x), 0);
130 | y_ticks.translate(d3.f(y), 1);
131 | ```
132 |
133 | HTML is supported as well! `translate` uses style transforms with px units if the first element in the selection is HTML.
134 |
135 | ```js
136 | svg_selection.translate([40,20]); // will set attribute transform="translate(40, 20)"
137 | html_selection.translate([40,20]); // will set style.transform = "translate(40px, 20px)"
138 | ```
139 |
140 | # selection.tspans(array) [<>](https://github.com/gka/d3-jetpack/blob/master/src/tspans.js "Source")
141 |
142 | For multi-line SVG text
143 |
144 | ```js
145 | selection.append('text')
146 | .tspans(function(d) {
147 | return d.text.split('\n');
148 | });
149 | selection.append('text').tspans(['Multiple', 'lines'], 20);
150 | ```
151 |
152 | The optional second argument sets the line height (defaults to 15).
153 |
154 | # d3.wordwrap(text, [lineWidth]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/wordwrap.js "Source")
155 |
156 | Comes in handy with the tspans:
157 |
158 | ```js
159 | selection.append('text')
160 | .tspans(function(d) {
161 | return d3.wordwrap(text, 15); // break line after 15 characters
162 | });
163 | ```
164 |
165 | # d3.f(key) [<>](https://github.com/gka/d3-jetpack/blob/master/src/f.js "Source")
166 |
167 | ``d3.f`` takes a string|number and returns a function that takes an object and returns whatever property the string is named. This clears away much of verbose function(d){ return ... } syntax in ECMAScript 5:
168 |
169 | ```js
170 | x.domain(d3.extent(items, function(d){ return d.price; }));
171 | ```
172 |
173 | becomes
174 |
175 | ```js
176 | x.domain(d3.extent(items, d3.f('price'));
177 | ```
178 |
179 | d3.f even accepts multiple accessors and will execute them in the order of appearance. So for instance, let's say we have an array of polygon objects like this ``{ points: [{x: 0, y: 3}, ...] }`` we can get the first ``y`` coordinates using:
180 |
181 | ```js
182 | var firstY = polygons.map(d3.f('points', 0, 'y'));
183 | ```
184 |
185 | Since we use this little function quite a lot, we usually set `var ƒ = d3.f` (type with [alt] + f on Macs). Also, [in @1wheel's blog](http://roadtolarissa.com/blog/2014/06/23/even-fewer-lamdas-with-d3/) you can read more about the rationale behind ƒ.
186 |
187 | # d3.ascendingKey(key) [<>](https://github.com/gka/d3-jetpack/blob/master/src/ascendingKey.js "Source")
188 |
189 | # d3.descendingKey(key) [<>](https://github.com/gka/d3-jetpack/blob/master/src/descendingKey.js "Source")
190 |
191 | These functions operate like d3.ascending / d3.descending but you can pass a key string or key function which will be used to specify the property by which to sort an array of objects.
192 |
193 | ```js
194 | var fruits = [{ name: "Apple", color: "green" }, { name: "Banana", color: "yellow" }];
195 | fruits.sort(d3.ascendingKey('color'));
196 | ```
197 |
198 | # d3.timer(callback[, delay[, time[, namespace]]]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/timer.js "Source")
199 |
200 | `d3.timer`, `d3.timeout` and `d3.interval` all now take an optional namespace argument. Previous timers with the same namespace as a new timer are stopped.
201 |
202 | # d3.nestBy(array, key) [<>](https://github.com/gka/d3-jetpack/blob/master/src/nestBy.js "Source")
203 |
204 | Shorthand for `d3.nest().key(key).entries(array)`. Returns an array of arrays, instead of a `key`/`value` pairs. The `key` property of each array is equal the value returned by the `key` function when it is called with element of the array.
205 |
206 | ```js
207 | d3.nest()
208 | .key(d => d.year)
209 | .entries(yields)
210 | .forEach(function(d){
211 | console.log('Count in ' + d.key + ': ' + d.values.length) })
212 | ```
213 |
214 | to
215 |
216 | ```js
217 | d3.nestBy(yields, d => d.year).forEach(function(d){
218 | console.log('Count in ' + d.key + ': ' + d.length) })
219 | ```
220 |
221 | # d3.loadData(file1, file2, file3, ..., callback) [<>](https://github.com/gka/d3-jetpack/blob/master/src/loadData.js "Source")
223 |
224 | Takes any number of files paths and loads them with `queue`, `d3.csv` and `d3.json`. After all the files have loaded, calls the `callback` function with the first error (or null if there are none) as the first argument and an array of the loaded files as the second. Instead of:
225 |
226 | ```js
227 | d3.queue()
228 | .defer(d3.csv, 'state-data.csv')
229 | .defer(d3.tsv, 'county-data.tsv')
230 | .defer(d3.json, 'us.json')
231 | .awaitAll(function(err, res){
232 | var states = res[0],
233 | counties = res[1],
234 | us = res[2]
235 | })
236 | ```
237 |
238 | if your file types match their extensions, you can use:
239 |
240 | ```js
241 | d3.loadData('state-data.csv', 'county-data.tsv', 'us.json', function(err, res){
242 | var states = res[0],
243 | counties = res[1],
244 | us = res[2]
245 | })
246 | ```
247 |
248 |
249 | # d3.round(x, precisions) [<>](https://github.com/gka/d3-jetpack/blob/master/src/round.js "Source")
250 |
251 | A useful short-hand method for `+d3.format('.'+precision+'f')(x)` also known as `+x.toFixed(precision)`. Note that this code is [fundamentally broken](https://twitter.com/mbostock/status/776448389814718465) but still works fine 99% of the time.
252 |
253 | ```js
254 | d3.round(1.2345, 2) // 1.23
255 | ```
256 |
257 |
258 | # d3.clamp(min, val, max) [<>](https://github.com/gka/d3-jetpack/blob/master/src/clamp.js "Source")
259 |
260 | Short for `Math.max(min, Math.min(max, val))`.
261 |
262 | ```js
263 | d3.clamp(0, -10, 200) // 0
264 | d3.clamp(0, 110, 200) // 110
265 | d3.clamp(0, 410, 200) // 200
266 | ```
267 |
268 |
269 | # d3.attachTooltip(selector) [<>](https://github.com/gka/d3-jetpack/blob/master/src/attachTooltip.js "Source")
270 |
271 | Attaches a light weight tooltip that prints out all of an objects properties on click. No more `> d3.select($0).datum()`!
272 |
273 | ```js
274 | d3.select('body').selectAppend('div.tooltip')
275 |
276 | circles.call(d3.attachTooltip)
277 | ```
278 |
279 | For formated tooltips, update the html of the tooltip on mouseover:
280 |
281 | ```js
282 | circles
283 | .call(d3.attachTooltip)
284 | .on('mouseover', function(d){
285 | d3.select('.tooltip').html("Number of " + d.key + ": " + d.length) })
286 | ```
287 |
288 | Make sure to add a `
` and that there's some tooltip css on the page:
289 |
290 | ```css
291 | .tooltip {
292 | top: -1000px;
293 | position: fixed;
294 | padding: 10px;
295 | background: rgba(255, 255, 255, .90);
296 | border: 1px solid lightgray;
297 | pointer-events: none;
298 | }
299 | .tooltip-hidden{
300 | opacity: 0;
301 | transition: all .3s;
302 | transition-delay: .1s;
303 | }
304 |
305 | @media (max-width: 590px){
306 | div.tooltip{
307 | bottom: -1px;
308 | width: calc(100%);
309 | left: -1px !important;
310 | right: -1px !important;
311 | top: auto !important;
312 | width: auto !important;
313 | }
314 | }
315 | ```
316 |
317 | # d3.conventions([options]) [<>](https://github.com/gka/d3-jetpack/blob/master/src/conventions.js "Source")
318 |
319 | `d3.conventions([config])` creates an SVG with a G element translated to follow the [margin convention](http://bl.ocks.org/mbostock/3019563). `d3.conventions` returns an object with the dimensions and location of the created element. Passing in a config object overrides the default dimensions.
320 |
321 | To create this html:
322 |
323 | ```html
324 |
325 |
328 |
329 | ```
330 |
331 | You could run this:
332 |
333 | ```js
334 | var sel = d3.select('#graph')
335 | var totalWidth = 900
336 | var totalHeight = 500
337 | var {svg, margin, height, width} = d3.conventions({sel, totalHeight, totalWidth})
338 |
339 | svg // d3 selection of G representing chart area
340 | margin // padding around G, defaults to {top: 20, left: 20, height: 20, width: 20}
341 | height // height of charting area (500 - 20 - 20 = 460 here)
342 | weight // width of charting area (900 - 20 - 20 = 460 here)
343 | ```
344 |
345 |
346 | `sel`: `d3.selection` of the element the SVG was appended to. Defaults to `d3.select("body")`, but can be specified by passing in an object: `d3.conventions({sel: d3.select("#graph-container")})` appends an SVG to `#graph-container`.
347 |
348 | `totalWidth`/`totalHeight`: size of the SVG. By default uses the offsetWidth and offsetHeight of `sel`. `d3.conventions({totalHeight: 500})` makes a responsive chart with a fixed height of 500.
349 |
350 | `margin`: Individual keys override the defaults. `d3.conventions({margins: {top: 50}})` sets the top margin to 50 and leaves the others at 20
351 |
352 | `width`/`height`: inner charting area. If passed into conventions, `totalWidth` and `totalHeight` are set to the extent of the charting area plus the margins. `d3.conventions({width: 200, height: 200, margin: {top: 50}})` creates a square charting area with extra top margin.
353 |
354 | `layers`: `d3.conventions` can also create multiple canvas and div elements. `d3.conventions({layers: 'sdc'})` makes an **S**VG, **D**IV and canvas **c**tx with the same margin and size. Layers are positioned absolutely on top of each other in the order listed in the layer string. To create an SVG with two canvas elements on top:
355 |
356 | ```js
357 | var {layers: [svg, bg_ctx, fg_ctx]} = d3.conventions({layers: 'scc'})
358 | ```
359 |
360 | `layers` defaults to `'s'`, creating a single SVG.
361 |
362 | Most charts use two linear scales and axii. `d3.conventions` returns some functions to get you started, but feel free to use something else!
363 |
364 | `x`: `scaleLinear().range([0, width])`. To use a different scale: `d3.conventions({x: d3.scaleSqrt()})`.
365 |
366 | `y`: `scaleLinear().range([height, 0])`.
367 |
368 | `xAxis`: `axisBottom().scale(x)`.
369 |
370 | `yAxis`: `axisLeft().scale(y)`.
371 |
372 | # d3.drawAxis({svg, xAxis, yAxis, height}) [<>](https://github.com/gka/d3-jetpack/blob/master/src/drawAxis.js "Source")
373 |
374 | Appends an `xAxis` aligned to the bottom of `svg` and a `yAxis` aligned to the left. You can pass the output of `conventions` directly to `drawAxis`, but make sure to set an `x` and `y` domain first!
375 |
376 | ```
377 | var c = d3.conventions()
378 | c.x.domain([1990, 2015])
379 | c.y.domain(d3.extent(data, d => d.cost))
380 | d3.drawAxis(c)
381 | ```
382 |
383 | ## Essential jetpack
384 |
385 | If you think jetpack adds too much to your build, try starting with the essential jetpack and adding features as you need them.
386 |
387 | ```js
388 | // essentials (insert, append, appendMany etc)
389 | import {f} from 'd3-jetpack/essentials';
390 | // get some extra stuff
391 | import attachTooltip from 'd3-jetpack/src/attachTooltip'
392 | ```
393 |
--------------------------------------------------------------------------------
/bin/rollup.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var rollup = require("rollup"),
4 | ascii = require("rollup-plugin-ascii"),
5 | nodeResolve = require("rollup-plugin-node-resolve");
6 |
7 | rollup.rollup({
8 | entry: "d3-index.js",
9 | plugins: [nodeResolve({jsnext: true}), ascii()]
10 | }).then(function(bundle) {
11 | return bundle.write({
12 | banner: process.argv[2],
13 | format: "umd",
14 | moduleName: "d3",
15 | dest: "build/d3v4+jetpack.js"
16 | });
17 | }).then(function() {
18 | console.warn("↳ build/d3v4+jetpack.js");
19 | }).catch(abort);
20 |
21 | function abort(error) {
22 | console.error(error.stack);
23 | }
24 |
--------------------------------------------------------------------------------
/build/d3-jetpack.cjs:
--------------------------------------------------------------------------------
1 | // https://github.com/gka/d3-jetpack#readme Version 2.2.0. Copyright 2021 Gregor Aisch.
2 | (function (global, factory) {
3 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-selection'), require('d3-transition'), require('d3-array'), require('d3-axis'), require('d3-scale'), require('d3-collection'), require('d3-queue'), require('d3-request'), require('d3-timer')) :
4 | typeof define === 'function' && define.amd ? define(['exports', 'd3-selection', 'd3-transition', 'd3-array', 'd3-axis', 'd3-scale', 'd3-collection', 'd3-queue', 'd3-request', 'd3-timer'], factory) :
5 | (factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3,global.d3,global.d3,global.d3,global.d3,global.d3));
6 | }(this, (function (exports,d3Selection,d3Transition,d3Array,d3Axis,d3Scale,d3Collection,d3Queue,d3Request,d3Timer) { 'use strict';
7 |
8 | var translateSelection = function(xy, dim) {
9 | var node = this.node();
10 | return !node ? this : node.getBBox ?
11 | this.attr('transform', function(d,i) {
12 | var p = typeof xy == 'function' ? xy.call(this, d,i) : xy;
13 | if (dim === 0) p = [p, 0]; else if (dim === 1) p = [0, p];
14 | return 'translate(' + p[0] +','+ p[1]+')';
15 | }) :
16 | this.style('transform', function(d,i) {
17 | var p = typeof xy == 'function' ? xy.call(this, d,i) : xy;
18 | if (dim === 0) p = [p, 0]; else if (dim === 1) p = [0, p];
19 | return 'translate(' + p[0] +'px,'+ p[1]+'px)';
20 | });
21 | };
22 |
23 | var parseAttributes = function(name) {
24 | if (typeof name === "string") {
25 | var attr = {},
26 | parts = name.split(/([\.#])/g), p;
27 | name = parts.shift();
28 | while ((p = parts.shift())) {
29 | if (p == '.') attr['class'] = attr['class'] ? attr['class'] + ' ' + parts.shift() : parts.shift();
30 | else if (p == '#') attr.id = parts.shift();
31 | }
32 | return {tag: name, attr: attr};
33 | }
34 | return name;
35 | };
36 |
37 | var append = function(name) {
38 | var create, n;
39 |
40 | if (typeof name === "function"){
41 | create = name;
42 | } else {
43 | n = parseAttributes(name);
44 | create = d3Selection.creator(n.tag);
45 | }
46 | var sel = this.select(function(){
47 | return this.appendChild(create.apply(this, arguments));
48 | });
49 |
50 | if (n) for (var key in n.attr) { sel.attr(key, n.attr[key]); }
51 | return sel;
52 | };
53 |
54 | function constantNull() {
55 | return null;
56 | }
57 |
58 | var insert = function(name, before) {
59 | var n = parseAttributes(name),
60 | create = d3Selection.creator(n.tag),
61 | select$$1 = before == null ? constantNull : typeof before === "function" ? before : d3Selection.selector(before);
62 |
63 | var s = this.select(function() {
64 | return this.insertBefore(create.apply(this, arguments), select$$1.apply(this, arguments) || null);
65 | });
66 |
67 |
68 | //attrs not provided by default in v4
69 | for (var key in n.attr) { s.attr(key, n.attr[key]); }
70 | return s;
71 | };
72 |
73 | var parent = function() {
74 | var parents = [];
75 | return this.filter(function() {
76 | if (parents.indexOf(this.parentNode) > -1) return false;
77 | parents.push(this.parentNode);
78 | return true;
79 | }).select(function() {
80 | return this.parentNode;
81 | });
82 | };
83 |
84 | var selectAppend = function(name) {
85 | var select$$1 = d3Selection.selector(name),
86 | n = parseAttributes(name), s;
87 |
88 | name = d3Selection.creator(n.tag);
89 |
90 | s = this.select(function() {
91 | return select$$1.apply(this, arguments) ||
92 | this.appendChild(name.apply(this, arguments));
93 | });
94 |
95 | //attrs not provided by default in v4
96 | for (var key in n.attr) { s.attr(key, n.attr[key]); }
97 | return s;
98 | };
99 |
100 | var tspans = function(lines, lh) {
101 | return this.selectAll('tspan')
102 | .data(function(d, i) {
103 | return (typeof(lines) === 'function' ? lines.call(this, d, i) : lines)
104 | .map(function(l) {
105 | return { line: l, parent: d };
106 | });
107 | })
108 | .enter()
109 | .append('tspan')
110 | .text(function(d) { return d.line; })
111 | .attr('x', 0)
112 | .attr('dy', function(d, i) { return i ? (typeof(lh) === 'function' ? lh.call(this, d.parent, d.line, i) : lh) || 15 : 0; });
113 | };
114 |
115 | var appendMany = function(name, data){
116 | if (typeof(data) == 'string'){
117 | console.warn("DEPRECATED: jetpack's appendMany order of arguments has changed. It's appendMany('div', data) from now on");
118 | var temp = data;
119 | data = name;
120 | name = temp;
121 | }
122 |
123 | return this.selectAll(null).data(data).enter().append(name);
124 | };
125 |
126 | const camelCaseAttrs = /^(allowReorder|attributeName|attributeType|autoReverse|baseFrequency|baseProfile|calcMode|clipPathUnits|contentScriptType|contentStyleType|diffuseConstant|edgeMode|externalResourcesRequired|filterRes|filterUnits|glyphRef|gradientTransform|gradientUnits|kernelMatrix|kernelUnitLength|keyPoints|keySplines|keyTimes|lengthAdjust|limitingConeAngle|markerHeight|markerUnits|markerWidth|maskContentUnits|maskUnits|numOctaves|pathLength|patternContentUnits|patternTransform|patternUnits|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|referrerPolicy|refX|refY|repeatCount|repeatDur|requiredExtensions|requiredFeatures|specularConstant|specularExponent|spreadMethod|startOffset|stdDeviation|stitchTiles|surfaceScale|systemLanguage|tableValues|targetX|targetY|textLength|viewBox|viewTarget|xChannelSelector|yChannelSelector|zoomAndPan)$/;
127 |
128 | var at = function(name, value) {
129 | if (typeof(name) == 'object'){
130 | for (var key in name){
131 | this.attr(camelCaseAttrs.test(key) ? key : key.replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase(), name[key]);
132 | }
133 | return this;
134 | } else{
135 | return arguments.length == 1 ? this.attr(name) : this.attr(name, value);
136 | }
137 | };
138 |
139 | function f(){
140 | var functions = arguments;
141 |
142 | //convert all string arguments into field accessors
143 | var i = 0, l = functions.length;
144 | while (i < l) {
145 | if (typeof(functions[i]) === 'string' || typeof(functions[i]) === 'number'){
146 | functions[i] = (function(str){ return function(d){ return d[str]; }; })(functions[i]);
147 | }
148 | i++;
149 | }
150 |
151 | //return composition of functions
152 | return function(d) {
153 | var i=0, l = functions.length;
154 | while (i++ < l) d = functions[i-1].call(this, d);
155 | return d;
156 | };
157 | }
158 |
159 | f.not = function(d){ return !d; };
160 | f.run = function(d){ return d(); };
161 | f.objToFn = function(obj, defaultVal){
162 | if (arguments.length == 1) defaultVal = undefined;
163 |
164 | return function(str){
165 | return typeof(obj[str]) !== 'undefined' ? obj[str] : defaultVal;
166 | };
167 | };
168 |
169 | var st = function(name, value) {
170 | if (typeof(name) == 'object'){
171 | for (var key in name){
172 | addStyle(this, key, name[key]);
173 | }
174 | return this;
175 | } else {
176 | return arguments.length == 1 ? this.style(name) : addStyle(this, name, value);
177 | }
178 |
179 |
180 | function addStyle(sel, style, value){
181 | style = style.replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
182 |
183 | var pxStyles = 'top left bottom right padding-top padding-left padding-bottom padding-right border-top b-width border-left-width border-botto-width m border-right-width margin-top margin-left margin-bottom margin-right font-size width stroke-width line-height margin padding border border-radius max-width min-width max-height min-height';
184 |
185 | if (~pxStyles.indexOf(style) ){
186 | sel.style(style, typeof value == 'function' ? wrapPx(value) : addPx(value));
187 | } else{
188 | sel.style(style, value);
189 | }
190 |
191 | return sel;
192 | }
193 |
194 | function addPx(d){ return d.match ? d : d + 'px'; }
195 | function wrapPx(fn){
196 | return function(){
197 | var val = fn.apply(this, arguments);
198 | return addPx(val)
199 | }
200 |
201 | }
202 | };
203 |
204 | // while this might not be reprentative for all fonts, it is
205 | // still better than assuming every character has the same width
206 | // (set monospace=true if you want to bypass this)
207 | var CHAR_W = {
208 | A:7,a:7,B:8,b:7,C:8,c:6,D:9,d:7,E:7,e:7,F:7,f:4,G:9,g:7,H:9,h:7,I:3,i:3,J:5,j:3,K:8,k:6,L:7,l:3,M:11,
209 | m:11,N:9,n:7,O:9,o:7,P:8,p:7,Q:9,q:7,R:8,r:4,S:8,s:6,T:7,t:4,U:9,u:7,V:7,v:6,W:11,w:9,X:7,x:6,Y:7,y:6,Z:7,z:5,
210 | '.':2,',':2,':':2,';':2
211 | };
212 |
213 | var wordwrap = function(line, maxCharactersPerLine, minCharactersPerLine, monospace) {
214 | var l, lines = [], w = [], words = [], w1, maxChars, minChars, maxLineW, minLineW;
215 | w1 = line.split(' ');
216 | w1.forEach(function(s, i) {
217 | var w2 = s.split('-');
218 | var lw = (i < w1.length - 1 ? ' ' : '');
219 | if (w2.length > 1) {
220 | w2.forEach(function(t, j) {
221 | w.push(t + (j < w2.length - 1 ? '-' : lw));
222 | });
223 | } else {
224 | w.push(s + lw);
225 | }
226 | });
227 | maxChars = maxCharactersPerLine || 40;
228 | minChars = minCharactersPerLine || Math.max(3, Math.min(maxChars * 0.5, 0.75 * w.map(word_len).sort(num_asc)[Math.round(w.length / 2)]));
229 | maxLineW = maxChars * CHAR_W.a;
230 | minLineW = minChars * CHAR_W.a;
231 | l = 0;
232 | w.forEach(function(d) {
233 | var ww = d3Array.sum(d.split('').map(char_w));
234 | if (l + ww > maxLineW && l > minLineW) {
235 | lines.push(words.join(''));
236 | words.length = 0;
237 | l = 0;
238 | }
239 | l += ww;
240 | return words.push(d);
241 | });
242 | if (words.length) {
243 | lines.push(words.join(''));
244 | }
245 | return lines.filter(function(d) {
246 | return d !== '';
247 | });
248 | function char_w(c) { return !monospace && CHAR_W[c] || CHAR_W.a; }
249 | function word_len(d) { return d.length; }
250 | function num_asc(a, b) { return a - b; }
251 | };
252 |
253 | var ascendingKey = function(key) {
254 | return typeof key == 'function' ? function (a, b) {
255 | return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : key(a) >= key(b) ? 0 : NaN;
256 | } : function (a, b) {
257 | return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : a[key] >= b[key] ? 0 : NaN;
258 | };
259 | };
260 |
261 | var descendingKey = function(key) {
262 | return typeof key == 'function' ? function (a, b) {
263 | return key(b) < key(a) ? -1 : key(b) > key(a) ? 1 : key(b) >= key(a) ? 0 : NaN;
264 | } : function (a, b) {
265 | return b[key] < a[key] ? -1 : b[key] > a[key] ? 1 : b[key] >= a[key] ? 0 : NaN;
266 | };
267 | };
268 |
269 | var conventions = function(c){
270 | c = c || {};
271 |
272 | c.margin = c.margin || {}
273 | ;['top', 'right', 'bottom', 'left'].forEach(function(d){
274 | if (!c.margin[d] && c.margin[d] !== 0) c.margin[d] = 20 ;
275 | });
276 |
277 | if (c.parentSel) c.sel = c.parentSel; // backwords comp
278 | var node = c.sel && c.sel.node();
279 |
280 | c.totalWidth = c.totalWidth || node && node.offsetWidth || 960;
281 | c.totalHeight = c.totalHeight || node && node.offsetHeight || 500;
282 |
283 | c.width = c.width || c.totalWidth - c.margin.left - c.margin.right;
284 | c.height = c.height || c.totalHeight - c.margin.top - c.margin.bottom;
285 |
286 | c.totalWidth = c.width + c.margin.left + c.margin.right;
287 | c.totalHeight = c.height + c.margin.top + c.margin.bottom;
288 |
289 | c.sel = c.sel || d3Selection.select('body');
290 | c.sel.st({position: 'relative', height: c.totalHeight, width: c.totalWidth});
291 |
292 | c.x = c.x || d3Scale.scaleLinear().range([0, c.width]);
293 | c.y = c.y || d3Scale.scaleLinear().range([c.height, 0]);
294 |
295 | c.xAxis = c.xAxis || d3Axis.axisBottom().scale(c.x);
296 | c.yAxis = c.yAxis || d3Axis.axisLeft().scale(c.y);
297 |
298 | c.layers = (c.layers || 's').split('').map(function(type){
299 | var layer;
300 | if (type == 's'){
301 | layer = c.sel.append('svg')
302 | .st({position: c.layers ? 'absolute' : ''})
303 | .attr('width', c.totalWidth)
304 | .attr('height', c.totalHeight)
305 | .append('g')
306 | .attr('transform', 'translate(' + c.margin.left + ',' + c.margin.top + ')');
307 |
308 | if (!c.svg) c.svg = layer; // defaults to lowest svg layer
309 | } else if (type == 'c'){
310 | var s = window.devicePixelRatio || 1;
311 |
312 | layer = c.sel.append('canvas')
313 | .at({width: c.totalWidth*s, height: c.totalHeight*s})
314 | .st({width: c.totalWidth, height: c.totalHeight})
315 | .st({position: 'absolute'})
316 | .node().getContext('2d');
317 | layer.scale(s, s);
318 | layer.translate(c.margin.left, c.margin.top);
319 | } else if (type == 'd'){
320 | layer = c.sel.append('div')
321 | .st({
322 | position: 'absolute',
323 | left: c.margin.left,
324 | top: c.margin.top,
325 | width: c.width,
326 | height: c.height
327 | });
328 | }
329 |
330 | return layer
331 | });
332 |
333 | return c;
334 | };
335 |
336 | var drawAxis = function(c){
337 | var xAxisSel = c.svg.append('g')
338 | .attr('class', 'x axis')
339 | .attr('transform', 'translate(0,' + c.height + ')')
340 | .call(c.xAxis);
341 |
342 | var yAxisSel = c.svg.append('g')
343 | .attr('class', 'y axis')
344 | .call(c.yAxis);
345 |
346 | return {xAxisSel: xAxisSel, yAxisSel: yAxisSel}
347 | };
348 |
349 | var clamp = function(min, d, max) {
350 | return Math.max(min, Math.min(max, d))
351 | };
352 |
353 | var attachTooltip = function(sel, tooltipSel, fieldFns){
354 | if (!sel.size()) return;
355 |
356 | tooltipSel = tooltipSel || d3Selection.select('.tooltip');
357 |
358 | sel
359 | .on('mouseover.attachTooltip', ttDisplay)
360 | .on('mousemove.attachTooltip', ttMove)
361 | .on('mouseout.attachTooltip', ttHide)
362 | .on('click.attachTooltip', function(d){ console.log(d); });
363 |
364 | var d = sel.datum();
365 | fieldFns = fieldFns || d3Collection.keys(d)
366 | .filter(function(str){
367 | return (typeof d[str] != 'object') && (d[str] != 'array');
368 | })
369 | .map(function(str){
370 | return function(d){ return str + ': ' + d[str] + ''; };
371 | });
372 |
373 | function ttDisplay(d){
374 | tooltipSel
375 | .classed('tooltip-hidden', false)
376 | .html('')
377 | .appendMany('div', fieldFns)
378 | .html(function(fn){ return fn(d); });
379 |
380 | d3Selection.select(this).classed('tooltipped', true);
381 | }
382 |
383 | function ttMove(d){
384 | if (!tooltipSel.size()) return;
385 |
386 | var e = d3Selection.event,
387 | x = e.clientX,
388 | y = e.clientY,
389 | bb = tooltipSel.node().getBoundingClientRect(),
390 | left = clamp(20, (x-bb.width/2), window.innerWidth - bb.width - 20),
391 | top = innerHeight > y + 20 + bb.height ? y + 20 : y - bb.height - 20;
392 |
393 | tooltipSel
394 | .style('left', left +'px')
395 | .style('top', top + 'px');
396 | }
397 |
398 | function ttHide(d){
399 | tooltipSel.classed('tooltip-hidden', true);
400 |
401 | d3Selection.selectAll('.tooltipped').classed('tooltipped', false);
402 | }
403 | };
404 |
405 | var loadData = function(){
406 | var q = d3Queue.queue();
407 |
408 | var args = [].slice.call(arguments);
409 | var files = args.slice(0, args.length - 1);
410 | var cb = args[args.length - 1];
411 |
412 | files.forEach(function(d){
413 | var type = d.split('?')[0].split('.').reverse()[0];
414 |
415 | var loadFn = {csv: d3Request.csv, tsv: d3Request.tsv, json: d3Request.json}[type];
416 | if (!loadFn) return cb(new Error('Invalid type', d));
417 | q.defer(loadFn, d) ;
418 | });
419 | q.awaitAll(cb);
420 | };
421 |
422 | var nestBy = function(array, key){
423 | return d3Collection.nest().key(key).entries(array).map(function(d){
424 | d.values.key = d.key;
425 | return d.values;
426 | });
427 | };
428 |
429 | var round = function(n, p) {
430 | return p ? Math.round(n * (p = Math.pow(10, p))) / p : Math.round(n);
431 | };
432 |
433 | // Clips the specified subject polygon to the specified clip polygon;
434 | // requires the clip polygon to be counterclockwise and convex.
435 | // https://en.wikipedia.org/wiki/Sutherland–Hodgman_algorithm
436 | var polygonClip = function(clip, subject) {
437 | var input,
438 | closed = polygonClosed(subject),
439 | i = -1,
440 | n = clip.length - polygonClosed(clip),
441 | j,
442 | m,
443 | a = clip[n - 1],
444 | b,
445 | c,
446 | d;
447 |
448 | while (++i < n) {
449 | input = subject.slice();
450 | subject.length = 0;
451 | b = clip[i];
452 | c = input[(m = input.length - closed) - 1];
453 | j = -1;
454 | while (++j < m) {
455 | d = input[j];
456 | if (polygonInside(d, a, b)) {
457 | if (!polygonInside(c, a, b)) {
458 | subject.push(polygonIntersect(c, d, a, b));
459 | }
460 | subject.push(d);
461 | } else if (polygonInside(c, a, b)) {
462 | subject.push(polygonIntersect(c, d, a, b));
463 | }
464 | c = d;
465 | }
466 | if (closed) subject.push(subject[0]);
467 | a = b;
468 | }
469 |
470 | return subject;
471 | };
472 |
473 | function polygonInside(p, a, b) {
474 | return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
475 | }
476 |
477 | // Intersect two infinite lines cd and ab.
478 | function polygonIntersect(c, d, a, b) {
479 | var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3,
480 | y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3,
481 | ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
482 | return [x1 + ua * x21, y1 + ua * y21];
483 | }
484 |
485 | // Returns true if the polygon is closed.
486 | function polygonClosed(coordinates) {
487 | var a = coordinates[0],
488 | b = coordinates[coordinates.length - 1];
489 | return !(a[0] - b[0] || a[1] - b[1]);
490 | }
491 |
492 | var prev = {};
493 |
494 | var timer$1 = function(fn, delay, time, name){
495 | if (prev[name]) prev[name].stop();
496 |
497 | var newTimer = d3Timer.timer(fn, delay, time, name);
498 | if (name) prev[name] = newTimer;
499 |
500 | return newTimer
501 | };
502 |
503 | var prev$1 = {};
504 |
505 | var interval$1 = function(fn, delay, time, name){
506 | if (prev$1[name]) prev$1[name].stop();
507 |
508 | var newTimer = d3Timer.interval(fn, delay, time, name);
509 | if (name) prev$1[name] = newTimer;
510 |
511 | return newTimer
512 | };
513 |
514 | var prev$2 = {};
515 |
516 | var timeout$1 = function(fn, delay, time, name){
517 | if (prev$2[name]) prev$2[name].stop();
518 |
519 | var newTimer = d3Timer.timeout(fn, delay, time, name);
520 | if (name) prev$2[name] = newTimer;
521 |
522 | return newTimer
523 | };
524 |
525 | d3Selection.selection.prototype.translate = translateSelection;
526 | d3Transition.transition.prototype.translate = translateSelection;
527 | d3Selection.selection.prototype.append = append;
528 | d3Selection.selection.prototype.insert = insert;
529 | d3Selection.selection.prototype.parent = parent;
530 | d3Selection.selection.prototype.selectAppend = selectAppend;
531 | d3Selection.selection.prototype.tspans = tspans;
532 | d3Selection.selection.prototype.appendMany = appendMany;
533 | d3Selection.selection.prototype.at = at;
534 | d3Selection.selection.prototype.st = st;
535 | d3Transition.transition.prototype.at = at;
536 | d3Transition.transition.prototype.st = st;
537 | d3Selection.selection.prototype.prop = d3Selection.selection.prototype.property;
538 |
539 | exports.wordwrap = wordwrap;
540 | exports.parseAttributes = parseAttributes;
541 | exports.f = f;
542 | exports.ascendingKey = ascendingKey;
543 | exports.descendingKey = descendingKey;
544 | exports.conventions = conventions;
545 | exports.drawAxis = drawAxis;
546 | exports.attachTooltip = attachTooltip;
547 | exports.loadData = loadData;
548 | exports.nestBy = nestBy;
549 | exports.round = round;
550 | exports.clamp = clamp;
551 | exports.polygonClip = polygonClip;
552 | exports.timer = timer$1;
553 | exports.interval = interval$1;
554 | exports.timeout = timeout$1;
555 |
556 | Object.defineProperty(exports, '__esModule', { value: true });
557 |
558 | })));
559 |
--------------------------------------------------------------------------------
/d3-index.js:
--------------------------------------------------------------------------------
1 | // export {
2 | // version
3 | // } from "./build/package";
4 |
5 | export * from "d3";
6 |
7 | import "d3-selection-multi";
8 | import "./index.js";
9 |
10 | export {
11 | wordwrap,
12 | parseAttributes,
13 | f,
14 | ascendingKey,
15 | descendingKey,
16 | conventions,
17 | drawAxis,
18 | attachTooltip,
19 | loadData,
20 | nestBy,
21 | round,
22 | clamp,
23 | polygonClip,
24 | timer,
25 | timeout,
26 | interval
27 | } from "./index.js";
28 |
--------------------------------------------------------------------------------
/essentials.js:
--------------------------------------------------------------------------------
1 | /*
2 | * the essential jetpack build. mainly to keep es6 imports small.
3 | * contains what's good about jetpack, except:
4 | * - transition hacks
5 | * - wordwrap
6 | * - conventions
7 | * - drawAxis
8 | * - attachTooltip
9 | * - loadData
10 | * - polygonClip
11 | * - timer, interval, timeout
12 | *
13 | * use at your own risk!
14 | */
15 |
16 | import {selection} from "d3-selection";
17 |
18 | import translateSelection from "./src/translate-selection.js";
19 | import append from "./src/append.js";
20 | import insert from "./src/insert.js";
21 | import parent from "./src/parent.js";
22 | import selectAppend from "./src/selectAppend.js";
23 | import tspans from "./src/tspans.js";
24 | import appendMany from "./src/appendMany.js";
25 | import at from "./src/at.js";
26 | import st from "./src/st.js";
27 |
28 | selection.prototype.translate = translateSelection;
29 | selection.prototype.append = append;
30 | selection.prototype.insert = insert;
31 | selection.prototype.parent = parent;
32 | selection.prototype.selectAppend = selectAppend;
33 | selection.prototype.tspans = tspans;
34 | selection.prototype.appendMany = appendMany;
35 | selection.prototype.at = at;
36 | selection.prototype.st = st;
37 | selection.prototype.prop = selection.prototype.property;
38 |
39 | export {default as parseAttributes} from "./src/parseAttributes.js";
40 | export {default as f} from "./src/f.js";
41 | export {default as ascendingKey} from "./src/ascendingKey.js";
42 | export {default as descendingKey} from "./src/descendingKey.js";
43 | export {default as nestBy} from "./src/nestBy.js";
44 | export {default as round} from "./src/round.js";
45 | export {default as clamp} from "./src/clamp.js";
46 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import {selection} from "d3-selection";
2 | import {transition} from "d3-transition";
3 |
4 | import translateSelection from "./src/translate-selection.js";
5 | import append from "./src/append.js";
6 | import insert from "./src/insert.js";
7 | import parent from "./src/parent.js";
8 | import selectAppend from "./src/selectAppend.js";
9 | import tspans from "./src/tspans.js";
10 | import appendMany from "./src/appendMany.js";
11 | import at from "./src/at.js";
12 | import st from "./src/st.js";
13 |
14 | selection.prototype.translate = translateSelection;
15 | transition.prototype.translate = translateSelection;
16 | selection.prototype.append = append;
17 | selection.prototype.insert = insert;
18 | selection.prototype.parent = parent;
19 | selection.prototype.selectAppend = selectAppend;
20 | selection.prototype.tspans = tspans;
21 | selection.prototype.appendMany = appendMany;
22 | selection.prototype.at = at;
23 | selection.prototype.st = st;
24 | transition.prototype.at = at;
25 | transition.prototype.st = st;
26 | selection.prototype.prop = selection.prototype.property;
27 |
28 | export {default as wordwrap} from "./src/wordwrap.js";
29 | export {default as parseAttributes} from "./src/parseAttributes.js";
30 | export {default as f} from "./src/f.js";
31 | export {default as ascendingKey} from "./src/ascendingKey.js";
32 | export {default as descendingKey} from "./src/descendingKey.js";
33 | export {default as conventions} from "./src/conventions.js";
34 | export {default as drawAxis} from "./src/drawAxis.js";
35 | export {default as attachTooltip} from "./src/attachTooltip.js";
36 | export {default as loadData} from "./src/loadData.js";
37 | export {default as nestBy} from "./src/nestBy.js";
38 | export {default as round} from "./src/round.js";
39 | export {default as clamp} from "./src/clamp.js";
40 | export {default as polygonClip} from "./src/polygonClip.js";
41 | export {default as timer} from "./src/timer.js";
42 | export {default as interval} from "./src/interval.js";
43 | export {default as timeout} from "./src/timeout.js";
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "d3-jetpack",
3 | "version": "2.2.0",
4 | "description": "d3-jetpack is a set of nifty convenience wrappers that speed up your daily work with d3.js",
5 | "type": "module",
6 | "main": "build/d3-jetpack.js",
7 | "jsnext:main": "index.js",
8 | "module": "index.js",
9 | "exports": {
10 | ".": {
11 | "import": "./index.js",
12 | "require": "./build/d3-jetpack.cjs"
13 | },
14 | "./essentials": "./essentials.js",
15 | "./selection": "./selection.js",
16 | "./package.json": "./package.json"
17 | },
18 | "scripts": {
19 | "pretest": "rm -rf build && mkdir build && rollup --banner \"$(preamble)\" -f umd -g d3-selection:d3,d3-transition:d3,d3-axis:d3,d3-scale:d3,d3-queue:d3,d3-array:d3,d3-collection:d3,d3-request:d3,d3-timer:d3 -n d3 -o build/d3-jetpack.cjs -- index.js",
20 | "test": "tape 'test/test-*.cjs'",
21 | "prepublish": "npm run pretest && bin/rollup.cjs"
22 | },
23 | "sideEffects": [
24 | "./index.js",
25 | "./essentials.js",
26 | "./selection.js"
27 | ],
28 | "devDependencies": {
29 | "d3": "4",
30 | "eslint": "2",
31 | "jsdom": "^16.6.0",
32 | "package-preamble": "0.0",
33 | "rollup": "0.41",
34 | "rollup-plugin-ascii": "0.0",
35 | "rollup-plugin-node-resolve": "2",
36 | "svgdom": "0.0.18",
37 | "tape": "^5.2.2",
38 | "uglify-js": "2"
39 | },
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/gka/d3-jetpack.git"
43 | },
44 | "keywords": [
45 | "d3",
46 | "d3-module"
47 | ],
48 | "author": {
49 | "name": "Gregor Aisch",
50 | "url": "https://driven-by-data.net/"
51 | },
52 | "license": "BSD-3-Clause",
53 | "bugs": {
54 | "url": "https://github.com/gka/d3-jetpack/issues"
55 | },
56 | "homepage": "https://github.com/gka/d3-jetpack#readme",
57 | "contributors": [
58 | {
59 | "name": "Gregor Aisch",
60 | "email": "gka@users.noreply.github.com",
61 | "url": "https://driven-by-data.net/"
62 | },
63 | {
64 | "name": "Adam Pearce",
65 | "email": "1wheel@gmail.com",
66 | "url": "https://roadtolarissa.com/"
67 | }
68 | ],
69 | "dependencies": {
70 | "d3-array": "1",
71 | "d3-axis": "1",
72 | "d3-collection": "1",
73 | "d3-queue": "3",
74 | "d3-request": "1",
75 | "d3-scale": "1",
76 | "d3-timer": "1"
77 | },
78 | "peerDependencies": {
79 | "d3-selection": "^1.4.2",
80 | "d3-selection-multi": "1",
81 | "d3-transition": "1"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/selection.js:
--------------------------------------------------------------------------------
1 | import './essentials.js';
2 |
3 | import {
4 | create,
5 | creator,
6 | matcher,
7 | select,
8 | selectAll,
9 | selection,
10 | selector,
11 | selectorAll,
12 | style,
13 | window,
14 | } from "d3-selection";
15 |
16 | export {
17 | create,
18 | creator,
19 | matcher,
20 | select,
21 | selectAll,
22 | selection,
23 | selector,
24 | selectorAll,
25 | style,
26 | window,
27 | };
28 |
--------------------------------------------------------------------------------
/src/append.js:
--------------------------------------------------------------------------------
1 | import {creator} from "d3-selection";
2 | import parseAttributes from "./parseAttributes.js";
3 |
4 | export default function(name) {
5 | var create, n;
6 |
7 | if (typeof name === "function"){
8 | create = name;
9 | } else {
10 | n = parseAttributes(name)
11 | create = creator(n.tag);
12 | }
13 | var sel = this.select(function(){
14 | return this.appendChild(create.apply(this, arguments));
15 | });
16 |
17 | if (n) for (var key in n.attr) { sel.attr(key, n.attr[key]); }
18 | return sel;
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/appendMany.js:
--------------------------------------------------------------------------------
1 | export default function(name, data){
2 | if (typeof(data) == 'string'){
3 | console.warn("DEPRECATED: jetpack's appendMany order of arguments has changed. It's appendMany('div', data) from now on")
4 | var temp = data
5 | data = name
6 | name = temp
7 | }
8 |
9 | return this.selectAll(null).data(data).enter().append(name);
10 | }
11 |
--------------------------------------------------------------------------------
/src/ascendingKey.js:
--------------------------------------------------------------------------------
1 | export default function(key) {
2 | return typeof key == 'function' ? function (a, b) {
3 | return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : key(a) >= key(b) ? 0 : NaN;
4 | } : function (a, b) {
5 | return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : a[key] >= b[key] ? 0 : NaN;
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/src/at.js:
--------------------------------------------------------------------------------
1 | const camelCaseAttrs = /^(allowReorder|attributeName|attributeType|autoReverse|baseFrequency|baseProfile|calcMode|clipPathUnits|contentScriptType|contentStyleType|diffuseConstant|edgeMode|externalResourcesRequired|filterRes|filterUnits|glyphRef|gradientTransform|gradientUnits|kernelMatrix|kernelUnitLength|keyPoints|keySplines|keyTimes|lengthAdjust|limitingConeAngle|markerHeight|markerUnits|markerWidth|maskContentUnits|maskUnits|numOctaves|pathLength|patternContentUnits|patternTransform|patternUnits|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|referrerPolicy|refX|refY|repeatCount|repeatDur|requiredExtensions|requiredFeatures|specularConstant|specularExponent|spreadMethod|startOffset|stdDeviation|stitchTiles|surfaceScale|systemLanguage|tableValues|targetX|targetY|textLength|viewBox|viewTarget|xChannelSelector|yChannelSelector|zoomAndPan)$/;
2 |
3 | export default function(name, value) {
4 | if (typeof(name) == 'object'){
5 | for (var key in name){
6 | this.attr(camelCaseAttrs.test(key) ? key : key.replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase(), name[key]);
7 | }
8 | return this;
9 | } else{
10 | return arguments.length == 1 ? this.attr(name) : this.attr(name, value);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/attachTooltip.js:
--------------------------------------------------------------------------------
1 | import {select} from 'd3-selection';
2 | import {selectAll} from 'd3-selection';
3 | import {event as d3event} from 'd3-selection';
4 | import {keys as d3keys} from 'd3-collection';
5 | import clamp from './clamp.js'
6 |
7 | export default function(sel, tooltipSel, fieldFns){
8 | if (!sel.size()) return;
9 |
10 | tooltipSel = tooltipSel || select('.tooltip');
11 |
12 | sel
13 | .on('mouseover.attachTooltip', ttDisplay)
14 | .on('mousemove.attachTooltip', ttMove)
15 | .on('mouseout.attachTooltip', ttHide)
16 | .on('click.attachTooltip', function(d){ console.log(d); });
17 |
18 | var d = sel.datum();
19 | fieldFns = fieldFns || d3keys(d)
20 | .filter(function(str){
21 | return (typeof d[str] != 'object') && (d[str] != 'array');
22 | })
23 | .map(function(str){
24 | return function(d){ return str + ': ' + d[str] + ''; };
25 | });
26 |
27 | function ttDisplay(d){
28 | tooltipSel
29 | .classed('tooltip-hidden', false)
30 | .html('')
31 | .appendMany('div', fieldFns)
32 | .html(function(fn){ return fn(d); });
33 |
34 | select(this).classed('tooltipped', true);
35 | }
36 |
37 | function ttMove(d){
38 | if (!tooltipSel.size()) return;
39 |
40 | var e = d3event,
41 | x = e.clientX,
42 | y = e.clientY,
43 | bb = tooltipSel.node().getBoundingClientRect(),
44 | left = clamp(20, (x-bb.width/2), window.innerWidth - bb.width - 20),
45 | top = innerHeight > y + 20 + bb.height ? y + 20 : y - bb.height - 20;
46 |
47 | tooltipSel
48 | .style('left', left +'px')
49 | .style('top', top + 'px');
50 | }
51 |
52 | function ttHide(d){
53 | tooltipSel.classed('tooltip-hidden', true);
54 |
55 | selectAll('.tooltipped').classed('tooltipped', false);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/clamp.js:
--------------------------------------------------------------------------------
1 | export default function(min, d, max) {
2 | return Math.max(min, Math.min(max, d))
3 | }
--------------------------------------------------------------------------------
/src/conventions.js:
--------------------------------------------------------------------------------
1 | import {axisLeft} from 'd3-axis';
2 | import {axisBottom} from 'd3-axis';
3 | import {select} from "d3-selection";
4 | import {scaleLinear} from "d3-scale";
5 |
6 | export default function(c){
7 | c = c || {};
8 |
9 | c.margin = c.margin || {}
10 | ;['top', 'right', 'bottom', 'left'].forEach(function(d){
11 | if (!c.margin[d] && c.margin[d] !== 0) c.margin[d] = 20 ;
12 | });
13 |
14 | if (c.parentSel) c.sel = c.parentSel // backwords comp
15 | var node = c.sel && c.sel.node()
16 |
17 | c.totalWidth = c.totalWidth || node && node.offsetWidth || 960;
18 | c.totalHeight = c.totalHeight || node && node.offsetHeight || 500;
19 |
20 | c.width = c.width || c.totalWidth - c.margin.left - c.margin.right;
21 | c.height = c.height || c.totalHeight - c.margin.top - c.margin.bottom;
22 |
23 | c.totalWidth = c.width + c.margin.left + c.margin.right;
24 | c.totalHeight = c.height + c.margin.top + c.margin.bottom;
25 |
26 | c.sel = c.sel || select('body');
27 | c.sel.st({position: 'relative', height: c.totalHeight, width: c.totalWidth})
28 |
29 | c.x = c.x || scaleLinear().range([0, c.width]);
30 | c.y = c.y || scaleLinear().range([c.height, 0]);
31 |
32 | c.xAxis = c.xAxis || axisBottom().scale(c.x);
33 | c.yAxis = c.yAxis || axisLeft().scale(c.y);
34 |
35 | c.layers = (c.layers || 's').split('').map(function(type){
36 | var layer
37 | if (type == 's'){
38 | layer = c.sel.append('svg')
39 | .st({position: c.layers ? 'absolute' : ''})
40 | .attr('width', c.totalWidth)
41 | .attr('height', c.totalHeight)
42 | .append('g')
43 | .attr('transform', 'translate(' + c.margin.left + ',' + c.margin.top + ')');
44 |
45 | if (!c.svg) c.svg = layer // defaults to lowest svg layer
46 | } else if (type == 'c'){
47 | var s = window.devicePixelRatio || 1
48 |
49 | layer = c.sel.append('canvas')
50 | .at({width: c.totalWidth*s, height: c.totalHeight*s})
51 | .st({width: c.totalWidth, height: c.totalHeight})
52 | .st({position: 'absolute'})
53 | .node().getContext('2d')
54 | layer.scale(s, s)
55 | layer.translate(c.margin.left, c.margin.top)
56 | } else if (type == 'd'){
57 | layer = c.sel.append('div')
58 | .st({
59 | position: 'absolute',
60 | left: c.margin.left,
61 | top: c.margin.top,
62 | width: c.width,
63 | height: c.height
64 | });
65 | }
66 |
67 | return layer
68 | })
69 |
70 | return c;
71 | }
72 |
--------------------------------------------------------------------------------
/src/descendingKey.js:
--------------------------------------------------------------------------------
1 | export default function(key) {
2 | return typeof key == 'function' ? function (a, b) {
3 | return key(b) < key(a) ? -1 : key(b) > key(a) ? 1 : key(b) >= key(a) ? 0 : NaN;
4 | } : function (a, b) {
5 | return b[key] < a[key] ? -1 : b[key] > a[key] ? 1 : b[key] >= a[key] ? 0 : NaN;
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/src/drawAxis.js:
--------------------------------------------------------------------------------
1 | export default function(c){
2 | var xAxisSel = c.svg.append('g')
3 | .attr('class', 'x axis')
4 | .attr('transform', 'translate(0,' + c.height + ')')
5 | .call(c.xAxis);
6 |
7 | var yAxisSel = c.svg.append('g')
8 | .attr('class', 'y axis')
9 | .call(c.yAxis);
10 |
11 | return {xAxisSel: xAxisSel, yAxisSel: yAxisSel}
12 | }
13 |
--------------------------------------------------------------------------------
/src/f.js:
--------------------------------------------------------------------------------
1 | function f(){
2 | var functions = arguments;
3 |
4 | //convert all string arguments into field accessors
5 | var i = 0, l = functions.length;
6 | while (i < l) {
7 | if (typeof(functions[i]) === 'string' || typeof(functions[i]) === 'number'){
8 | functions[i] = (function(str){ return function(d){ return d[str]; }; })(functions[i]);
9 | }
10 | i++;
11 | }
12 |
13 | //return composition of functions
14 | return function(d) {
15 | var i=0, l = functions.length;
16 | while (i++ < l) d = functions[i-1].call(this, d);
17 | return d;
18 | };
19 | }
20 |
21 | f.not = function(d){ return !d; };
22 | f.run = function(d){ return d(); };
23 | f.objToFn = function(obj, defaultVal){
24 | if (arguments.length == 1) defaultVal = undefined;
25 |
26 | return function(str){
27 | return typeof(obj[str]) !== 'undefined' ? obj[str] : defaultVal;
28 | };
29 | };
30 |
31 | export default f;
32 |
--------------------------------------------------------------------------------
/src/insert.js:
--------------------------------------------------------------------------------
1 | import {creator, selector} from "d3-selection";
2 | import parseAttributes from "./parseAttributes.js";
3 |
4 | function constantNull() {
5 | return null;
6 | }
7 |
8 | export default function(name, before) {
9 | var n = parseAttributes(name),
10 | create = creator(n.tag),
11 | select = before == null ? constantNull : typeof before === "function" ? before : selector(before);
12 |
13 | var s = this.select(function() {
14 | return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);
15 | });
16 |
17 |
18 | //attrs not provided by default in v4
19 | for (var key in n.attr) { s.attr(key, n.attr[key]); }
20 | return s;
21 | }
--------------------------------------------------------------------------------
/src/interval.js:
--------------------------------------------------------------------------------
1 | import {interval} from "d3-timer";
2 |
3 | var prev = {}
4 |
5 | export default function(fn, delay, time, name){
6 | if (prev[name]) prev[name].stop()
7 |
8 | var newTimer = interval(fn, delay, time, name)
9 | if (name) prev[name] = newTimer
10 |
11 | return newTimer
12 | }
13 |
--------------------------------------------------------------------------------
/src/loadData.js:
--------------------------------------------------------------------------------
1 | import {queue} from 'd3-queue';
2 | import {csv, tsv, json} from 'd3-request';
3 |
4 | export default function(){
5 | var q = queue();
6 |
7 | var args = [].slice.call(arguments);
8 | var files = args.slice(0, args.length - 1);
9 | var cb = args[args.length - 1];
10 |
11 | files.forEach(function(d){
12 | var type = d.split('?')[0].split('.').reverse()[0];
13 |
14 | var loadFn = {csv: csv, tsv: tsv, json: json}[type];
15 | if (!loadFn) return cb(new Error('Invalid type', d));
16 | q.defer(loadFn, d) ;
17 | });
18 | q.awaitAll(cb);
19 | }
20 |
--------------------------------------------------------------------------------
/src/nestBy.js:
--------------------------------------------------------------------------------
1 | import {nest} from 'd3-collection';
2 |
3 | export default function(array, key){
4 | return nest().key(key).entries(array).map(function(d){
5 | d.values.key = d.key;
6 | return d.values;
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/src/parent.js:
--------------------------------------------------------------------------------
1 | import {creator} from "d3-selection";
2 |
3 | export default function() {
4 | var parents = [];
5 | return this.filter(function() {
6 | if (parents.indexOf(this.parentNode) > -1) return false;
7 | parents.push(this.parentNode);
8 | return true;
9 | }).select(function() {
10 | return this.parentNode;
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/parseAttributes.js:
--------------------------------------------------------------------------------
1 | export default function(name) {
2 | if (typeof name === "string") {
3 | var attr = {},
4 | parts = name.split(/([\.#])/g), p;
5 | name = parts.shift();
6 | while ((p = parts.shift())) {
7 | if (p == '.') attr['class'] = attr['class'] ? attr['class'] + ' ' + parts.shift() : parts.shift();
8 | else if (p == '#') attr.id = parts.shift();
9 | }
10 | return {tag: name, attr: attr};
11 | }
12 | return name;
13 | }
14 |
--------------------------------------------------------------------------------
/src/polygonClip.js:
--------------------------------------------------------------------------------
1 | // Clips the specified subject polygon to the specified clip polygon;
2 | // requires the clip polygon to be counterclockwise and convex.
3 | // https://en.wikipedia.org/wiki/Sutherland–Hodgman_algorithm
4 | export default function(clip, subject) {
5 | var input,
6 | closed = polygonClosed(subject),
7 | i = -1,
8 | n = clip.length - polygonClosed(clip),
9 | j,
10 | m,
11 | a = clip[n - 1],
12 | b,
13 | c,
14 | d;
15 |
16 | while (++i < n) {
17 | input = subject.slice();
18 | subject.length = 0;
19 | b = clip[i];
20 | c = input[(m = input.length - closed) - 1];
21 | j = -1;
22 | while (++j < m) {
23 | d = input[j];
24 | if (polygonInside(d, a, b)) {
25 | if (!polygonInside(c, a, b)) {
26 | subject.push(polygonIntersect(c, d, a, b));
27 | }
28 | subject.push(d);
29 | } else if (polygonInside(c, a, b)) {
30 | subject.push(polygonIntersect(c, d, a, b));
31 | }
32 | c = d;
33 | }
34 | if (closed) subject.push(subject[0]);
35 | a = b;
36 | }
37 |
38 | return subject;
39 | }
40 |
41 | function polygonInside(p, a, b) {
42 | return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]);
43 | }
44 |
45 | // Intersect two infinite lines cd and ab.
46 | function polygonIntersect(c, d, a, b) {
47 | var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3,
48 | y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3,
49 | ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
50 | return [x1 + ua * x21, y1 + ua * y21];
51 | }
52 |
53 | // Returns true if the polygon is closed.
54 | function polygonClosed(coordinates) {
55 | var a = coordinates[0],
56 | b = coordinates[coordinates.length - 1];
57 | return !(a[0] - b[0] || a[1] - b[1]);
58 | }
59 |
--------------------------------------------------------------------------------
/src/round.js:
--------------------------------------------------------------------------------
1 | export default function(n, p) {
2 | return p ? Math.round(n * (p = Math.pow(10, p))) / p : Math.round(n);
3 | }
4 |
--------------------------------------------------------------------------------
/src/selectAppend.js:
--------------------------------------------------------------------------------
1 | import {selector} from "d3-selection";
2 | import {creator} from "d3-selection";
3 | import parseAttributes from "./parseAttributes.js";
4 |
5 | export default function(name) {
6 | var select = selector(name),
7 | n = parseAttributes(name), s;
8 |
9 | name = creator(n.tag);
10 |
11 | s = this.select(function() {
12 | return select.apply(this, arguments) ||
13 | this.appendChild(name.apply(this, arguments));
14 | });
15 |
16 | //attrs not provided by default in v4
17 | for (var key in n.attr) { s.attr(key, n.attr[key]); }
18 | return s;
19 | }
20 |
--------------------------------------------------------------------------------
/src/st.js:
--------------------------------------------------------------------------------
1 | import ƒ from "./f.js";
2 |
3 | export default function(name, value) {
4 | if (typeof(name) == 'object'){
5 | for (var key in name){
6 | addStyle(this, key, name[key]);
7 | }
8 | return this;
9 | } else {
10 | return arguments.length == 1 ? this.style(name) : addStyle(this, name, value);
11 | }
12 |
13 |
14 | function addStyle(sel, style, value){
15 | style = style.replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
16 |
17 | var pxStyles = 'top left bottom right padding-top padding-left padding-bottom padding-right border-top b-width border-left-width border-botto-width m border-right-width margin-top margin-left margin-bottom margin-right font-size width stroke-width line-height margin padding border border-radius max-width min-width max-height min-height';
18 |
19 | if (~pxStyles.indexOf(style) ){
20 | sel.style(style, typeof value == 'function' ? wrapPx(value) : addPx(value));
21 | } else{
22 | sel.style(style, value);
23 | }
24 |
25 | return sel;
26 | }
27 |
28 | function addPx(d){ return d.match ? d : d + 'px'; }
29 | function wrapPx(fn){
30 | return function(){
31 | var val = fn.apply(this, arguments)
32 | return addPx(val)
33 | }
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/timeout.js:
--------------------------------------------------------------------------------
1 | import {timeout} from "d3-timer";
2 |
3 | var prev = {}
4 |
5 | export default function(fn, delay, time, name){
6 | if (prev[name]) prev[name].stop()
7 |
8 | var newTimer = timeout(fn, delay, time, name)
9 | if (name) prev[name] = newTimer
10 |
11 | return newTimer
12 | }
13 |
--------------------------------------------------------------------------------
/src/timer.js:
--------------------------------------------------------------------------------
1 | import {timer} from "d3-timer";
2 |
3 | var prev = {}
4 |
5 | export default function(fn, delay, time, name){
6 | if (prev[name]) prev[name].stop()
7 |
8 | var newTimer = timer(fn, delay, time, name)
9 | if (name) prev[name] = newTimer
10 |
11 | return newTimer
12 | }
13 |
--------------------------------------------------------------------------------
/src/translate-selection.js:
--------------------------------------------------------------------------------
1 | export default function(xy, dim) {
2 | var node = this.node()
3 | return !node ? this : node.getBBox ?
4 | this.attr('transform', function(d,i) {
5 | var p = typeof xy == 'function' ? xy.call(this, d,i) : xy;
6 | if (dim === 0) p = [p, 0]; else if (dim === 1) p = [0, p];
7 | return 'translate(' + p[0] +','+ p[1]+')';
8 | }) :
9 | this.style('transform', function(d,i) {
10 | var p = typeof xy == 'function' ? xy.call(this, d,i) : xy;
11 | if (dim === 0) p = [p, 0]; else if (dim === 1) p = [0, p];
12 | return 'translate(' + p[0] +'px,'+ p[1]+'px)';
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/tspans.js:
--------------------------------------------------------------------------------
1 | export default function(lines, lh) {
2 | return this.selectAll('tspan')
3 | .data(function(d, i) {
4 | return (typeof(lines) === 'function' ? lines.call(this, d, i) : lines)
5 | .map(function(l) {
6 | return { line: l, parent: d };
7 | });
8 | })
9 | .enter()
10 | .append('tspan')
11 | .text(function(d) { return d.line; })
12 | .attr('x', 0)
13 | .attr('dy', function(d, i) { return i ? (typeof(lh) === 'function' ? lh.call(this, d.parent, d.line, i) : lh) || 15 : 0; });
14 | }
15 |
--------------------------------------------------------------------------------
/src/wordwrap.js:
--------------------------------------------------------------------------------
1 | import {sum} from 'd3-array';
2 |
3 | // while this might not be reprentative for all fonts, it is
4 | // still better than assuming every character has the same width
5 | // (set monospace=true if you want to bypass this)
6 | var CHAR_W = {
7 | A:7,a:7,B:8,b:7,C:8,c:6,D:9,d:7,E:7,e:7,F:7,f:4,G:9,g:7,H:9,h:7,I:3,i:3,J:5,j:3,K:8,k:6,L:7,l:3,M:11,
8 | m:11,N:9,n:7,O:9,o:7,P:8,p:7,Q:9,q:7,R:8,r:4,S:8,s:6,T:7,t:4,U:9,u:7,V:7,v:6,W:11,w:9,X:7,x:6,Y:7,y:6,Z:7,z:5,
9 | '.':2,',':2,':':2,';':2
10 | };
11 |
12 | export default function(line, maxCharactersPerLine, minCharactersPerLine, monospace) {
13 | var l, lines = [], w = [], words = [], w1, maxChars, minChars, maxLineW, minLineW;
14 | w1 = line.split(' ');
15 | w1.forEach(function(s, i) {
16 | var w2 = s.split('-');
17 | var lw = (i < w1.length - 1 ? ' ' : '');
18 | if (w2.length > 1) {
19 | w2.forEach(function(t, j) {
20 | w.push(t + (j < w2.length - 1 ? '-' : lw));
21 | });
22 | } else {
23 | w.push(s + lw);
24 | }
25 | });
26 | maxChars = maxCharactersPerLine || 40;
27 | minChars = minCharactersPerLine || Math.max(3, Math.min(maxChars * 0.5, 0.75 * w.map(word_len).sort(num_asc)[Math.round(w.length / 2)]));
28 | maxLineW = maxChars * CHAR_W.a;
29 | minLineW = minChars * CHAR_W.a;
30 | l = 0;
31 | w.forEach(function(d) {
32 | var ww = sum(d.split('').map(char_w));
33 | if (l + ww > maxLineW && l > minLineW) {
34 | lines.push(words.join(''));
35 | words.length = 0;
36 | l = 0;
37 | }
38 | l += ww;
39 | return words.push(d);
40 | });
41 | if (words.length) {
42 | lines.push(words.join(''));
43 | }
44 | return lines.filter(function(d) {
45 | return d !== '';
46 | });
47 | function char_w(c) { return !monospace && CHAR_W[c] || CHAR_W.a; }
48 | function word_len(d) { return d.length; }
49 | function num_asc(a, b) { return a - b; }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/test/helpers/makeDocument.cjs:
--------------------------------------------------------------------------------
1 | var JSDOM = require('jsdom').JSDOM;
2 |
3 | function makeDocument(fragment) {
4 | var dom = new JSDOM('' + fragment);
5 | return dom.window.document;
6 | }
7 |
8 | module.exports = makeDocument;
9 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/test/test-append.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | makeDocument = require('./helpers/makeDocument.cjs'),
3 | jetpack = require('../build/d3-jetpack.cjs'),
4 | d3 = require('d3-selection');
5 |
6 |
7 | tape('append adds a class and id', function(test) {
8 | var document = makeDocument('');
9 |
10 | d3.select(document.querySelector('div')).append('span#id.class');
11 |
12 | var span = document.querySelector('span');
13 | test.equal(span.getAttribute('id'), 'id');
14 | test.equal(span.getAttribute('class'), 'class');
15 | test.end();
16 | });
17 |
18 |
19 | tape('append takes a function', function(test) {
20 | var document = makeDocument('');
21 |
22 | d3.select(document.querySelector('div'))
23 | .append(d => document.createElement('span'))
24 | .attr('id', 'id')
25 | .attr('class', 'class')
26 |
27 | var span = document.querySelector('span');
28 | test.equal(span.getAttribute('id'), 'id');
29 | test.equal(span.getAttribute('class'), 'class');
30 | test.end();
31 | });
--------------------------------------------------------------------------------
/test/test-at.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | makeDocument = require('./helpers/makeDocument.cjs'),
4 | d3 = require('d3-selection');
5 |
6 | tape('at can look up attributes', function(test) {
7 | var document = makeDocument('');
8 |
9 | var prop = d3.select(document.querySelector('div')).at('prop')
10 | test.equal(prop, 'propVal');
11 | test.end();
12 | });
13 |
14 | tape('at can set attributes', function(test) {
15 | var document = makeDocument('');
16 |
17 | d3.select(document.querySelector('div')).at('prop', 'propVal');
18 |
19 | test.equal(document.querySelector('div').getAttribute('prop'), 'propVal');
20 | test.end();
21 | });
22 |
23 | tape('at can set attributes with an object', function(test) {
24 | var document = makeDocument('');
25 |
26 | d3.select(document.querySelector('div')).at({prop: 'propVal', width: 100});
27 |
28 | test.equal(document.querySelector('div').getAttribute('prop'), 'propVal');
29 | test.equal(document.querySelector('div').getAttribute('width'), '100');
30 | test.end();
31 | });
32 |
33 | tape('camelcase is converted to hypens', function(test) {
34 | var document = makeDocument('');
35 |
36 | d3.select(document.querySelector('div')).at({fillOpacity: 'propVal', maxWidth: 100});
37 |
38 | test.equal(document.querySelector('div').getAttribute('fill-opacity'), 'propVal');
39 | test.equal(document.querySelector('div').getAttribute('max-width'), '100');
40 | test.end();
41 | });
42 |
43 | tape('blacklisted camelcase attrs are not hyphenized', function(test) {
44 | var document = makeDocument('');
45 |
46 | d3.select(document.querySelector('div')).at({viewBox: '0 0 10 10', markerWidth: '10'});
47 |
48 | test.equal(document.querySelector('div').getAttribute('viewBox'), '0 0 10 10');
49 | test.equal(document.querySelector('div').getAttribute('markerWidth'), '10');
50 | test.end();
51 | });
52 |
--------------------------------------------------------------------------------
/test/test-clamp.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs');
3 |
4 | tape('clamp uses min', function(test) {
5 | test.equal(jetpack.clamp(0, -1, 10), 0);
6 | test.end();
7 | });
8 |
9 | tape('clamp uses max', function(test) {
10 | test.equal(jetpack.clamp(-10, -1, 1), -1);
11 | test.end();
12 | });
13 |
14 | tape('clamp uses middle', function(test) {
15 | test.equal(jetpack.clamp(10, 30, 100), 30);
16 | test.end();
17 | });
18 |
--------------------------------------------------------------------------------
/test/test-f.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | ƒ = jetpack.f
4 |
5 | tape('ƒ with no arguements is the identity function', function(test) {
6 | test.equal(ƒ()(10), 10);
7 | test.end();
8 | });
9 |
10 | tape('strings return object accessors', function(test) {
11 | test.equal(ƒ('prop')({prop: 'myProp'}), 'myProp');
12 | test.end();
13 | });
14 |
15 | tape('function are composed', function(test) {
16 | function addOne(d){ return d + 1 }
17 | test.equal(ƒ('num', addOne)({num: 10.3}), 11.3);
18 | test.end();
19 | });
20 |
21 | tape('function are composed', function(test) {
22 | function addOne(d){ return d + 1 }
23 | test.equal(ƒ(addOne, addOne)(10) , 12);
24 | test.end();
25 | });
--------------------------------------------------------------------------------
/test/test-nestBy.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs');
3 |
4 | tape('nestBy sets returns an array of arrays with key props', function(test) {
5 | var bySign = jetpack.nestBy([1, 2, 3, 4], function(d){ return d > 0 })
6 | console.log(bySign[0].key)
7 | test.equal(bySign[0].length, 4);
8 | test.equal(bySign[0].key, 'true');
9 | test.end();
10 | });
11 |
12 | tape('nestBy divides items into seperate groups', function(test) {
13 | var byEven = jetpack.nestBy([1, 2, 3, 4], function(d){ return d % 2 == 0 })
14 | test.equal(byEven[0].length, 2);
15 | test.equal(byEven[1].length, 2);
16 | test.equal(byEven[0].key == 'true', byEven[1].key != 'true');
17 | test.end();
18 | });
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/test-parseAttributes.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs');
3 |
4 | tape('parseAttributes can read a class and id', function(test) {
5 | var obj = jetpack.parseAttributes('div.className#idName');
6 | test.equal(obj.tag, 'div');
7 | test.equal(obj.attr.class, 'className');
8 | test.equal(obj.attr.id, 'idName');
9 | test.end();
10 | });
11 |
12 |
13 | tape('parseAttributes can read multiple classes', function(test) {
14 | var obj = jetpack.parseAttributes('div.class1.class2');
15 | test.equal(obj.attr.class, 'class1 class2');
16 | test.end();
17 | });
18 |
19 | tape('parseAttributes can read tags', function(test) {
20 | var obj = jetpack.parseAttributes('span');
21 | test.equal(obj.tag, 'span');
22 | test.end();
23 | });
--------------------------------------------------------------------------------
/test/test-selectAppend.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | makeDocument = require('./helpers/makeDocument.cjs'),
4 |
5 | d3 = require('d3-selection');
6 |
7 |
8 | tape('selectAppend selects when element exists', function(test) {
9 | var document = makeDocument('
');
10 |
11 | var span = document.querySelector('span')
12 |
13 | var d3Span = d3.select(document.querySelector('div'))
14 | .selectAppend('span').node();
15 |
16 | test.equal(span, d3Span);
17 | test.end();
18 | });
19 |
20 | tape('selectAppend appends when element doesn\'t exist', function(test) {
21 | var document = makeDocument('');
22 |
23 | var d3Span = d3.select(document.querySelector('div'))
24 | .selectAppend('span').node();
25 |
26 | var span = document.querySelector('span')
27 |
28 | test.equal(span, d3Span);
29 | test.end();
30 | });
31 |
32 | tape('selectAppend selects each child when element exists', function(test) {
33 | var document = makeDocument('
');
34 |
35 | var spans = document.querySelectorAll('span')
36 |
37 | var d3Spans = d3.select(document).selectAll('div')
38 | .selectAppend('span');
39 |
40 | d3Spans.each(function(d, i) {
41 | test.equal(spans[i], this);
42 | })
43 |
44 | test.end();
45 | });
46 |
47 | tape('selectAppend append each child when element exists', function(test) {
48 | var document = makeDocument('');
49 |
50 | var d3Spans = d3.select(document).selectAll('div')
51 | .selectAppend('span');
52 |
53 | var spans = document.querySelectorAll('span')
54 |
55 | d3Spans.each(function(d, i) {
56 | test.equal(spans[i], this);
57 | })
58 |
59 | test.end();
60 | });
61 |
62 | tape('selectAppend should select or append each child element based on whether they exist', function(test) {
63 | var document = makeDocument('
');
64 |
65 | var d3Spans = d3.select(document).selectAll('div')
66 | .selectAppend('span');
67 |
68 | var spans = document.querySelectorAll('span')
69 |
70 | test.equal(d3Spans.size(), 2);
71 |
72 | d3Spans.each(function(d, i) {
73 | test.equal(spans[i], this);
74 | })
75 |
76 | test.end();
77 | });
78 |
79 |
80 | tape('selectAppend adds a class and id', function(test) {
81 | var document = makeDocument('');
82 |
83 | d3.select(document.querySelector('div')).selectAppend('span#id.class');
84 |
85 | var span = document.querySelector('span');
86 | test.equal(span.getAttribute('id'), 'id');
87 | test.equal(span.getAttribute('class'), 'class');
88 | test.end();
89 | });
90 |
--------------------------------------------------------------------------------
/test/test-st.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | makeDocument = require('./helpers/makeDocument.cjs'),
4 | d3 = require('d3-selection');
5 |
6 | tape('st can look up styles', function(test) {
7 | var document = makeDocument('')
8 |
9 | var prop = d3.select(document.querySelector('div')).st('background')
10 | test.equal(prop, 'green')
11 | test.end()
12 | })
13 |
14 | tape('st can set styles', function(test) {
15 | var document = makeDocument('')
16 |
17 | d3.select(document.querySelector('div')).st('background', 'green')
18 |
19 | test.equal(d3.select(document.querySelector('div')).st('background'), 'green')
20 | test.end()
21 | })
22 |
23 | tape('st can set style with an object', function(test) {
24 | var document = makeDocument('')
25 |
26 | var sel = d3.select(document.querySelector('div'))
27 |
28 | sel.st({color: 'blue', width: '100px'})
29 |
30 | test.equal(sel.style('color'), 'blue')
31 | test.equal(sel.style('width'), '100px')
32 | test.end()
33 | })
34 |
35 | tape('camelcase is converted to hypens', function(test) {
36 | var document = makeDocument('')
37 |
38 | var sel = d3.select(document.querySelector('div'))
39 |
40 | sel.st({fillOpacity: 1, maxWidth: '100px'})
41 |
42 | test.equal(sel.style('fill-opacity'), '1')
43 | test.equal(sel.style('max-width'), '100px')
44 | test.end()
45 | })
46 |
47 | tape('px is appened to numbers', function(test) {
48 | var document = makeDocument('')
49 |
50 | var sel = d3.select(document.querySelector('div'))
51 |
52 | sel.st({margin: 1, maxWidth: 100})
53 | sel.st('height', 20)
54 |
55 | test.equal(sel.style('margin'), '1px')
56 | test.equal(sel.style('max-width'), '100px')
57 | test.equal(sel.style('height'), '20px')
58 | test.end()
59 | })
--------------------------------------------------------------------------------
/test/test-translate-selection.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | makeDocument = require('./helpers/makeDocument.cjs'),
4 | svgdom = require('svgdom'),
5 | d3 = require('d3-selection');
6 |
7 | // svg translate
8 |
9 | tape('translate can take an array and set transform attr on svg element', function(test) {
10 | var svg = svgdom.document.documentElement;
11 | d3.select(svg).translate([10, 10]);
12 | test.equal(svg.getAttribute('transform'), 'translate(10,10)');
13 | test.end();
14 | });
15 |
16 | tape('translate can take an array and set transform attr on svg element', function(test) {
17 | var svg = svgdom.document.documentElement;
18 | d3.select(svg).translate(function(){ return [10, 10]; });
19 | test.equal(svg.getAttribute('transform'), 'translate(10,10)');
20 | test.end();
21 | });
22 |
23 | tape('translate can take dim parameter to work on single dimension - svg', function(test) {
24 | var svg = svgdom.document.documentElement;
25 | d3.select(svg).translate(10, 0);
26 | test.equal(svg.getAttribute('transform'), 'translate(10,0)');
27 | test.end();
28 | });
29 |
30 | tape('translate can take dim parameter to work on single dimension - svg', function(test) {
31 | var svg = svgdom.document.documentElement;
32 | d3.select(svg).translate(function() { return 10; }, 1);
33 | test.equal(svg.getAttribute('transform'), 'translate(0,10)');
34 | test.end();
35 | });
36 |
37 | // html translate
38 |
39 | tape('translate can take an array and set transform style on html element', function(test) {
40 | var document = makeDocument();
41 | d3.select(document.body).translate([10, 10]);
42 | test.equal(document.body.style.transform, 'translate(10px,10px)');
43 | test.end();
44 | });
45 |
46 | tape('translate can take a function and set transform string on html element', function(test) {
47 | var document = makeDocument();
48 | d3.select(document.body).translate(function(){ return [10, 10]; });
49 | test.equal(document.body.style.transform, 'translate(10px,10px)');
50 | test.end();
51 | });
52 |
53 | tape('translate can take dim parameter to work on single dimension - html', function(test) {
54 | var document = makeDocument();
55 | d3.select(document.body).translate(10, 1);
56 | test.equal(document.body.style.transform, 'translate(0px,10px)');
57 | test.end();
58 | });
59 |
60 | tape('translate can take dim parameter to work on single dimension - html', function(test) {
61 | var document = makeDocument();
62 | d3.select(document.body).translate(function() { return 10; }, 0);
63 | test.equal(document.body.style.transform, 'translate(10px,0px)');
64 | test.end();
65 | });
66 |
67 |
68 | tape('translate does not break on empty selections', function(test) {
69 | var document = makeDocument();
70 | d3.select(document.body)
71 | .select('.no-element')
72 | .translate(function() { return 10; }, 0);
73 | test.end();
74 | });
75 |
--------------------------------------------------------------------------------
/test/test-tspans.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs'),
3 | makeDocument = require('./helpers/makeDocument.cjs'),
4 | d3 = require('d3-selection');
5 |
6 |
7 | tape('tspans adds a tspans and sets text', function(test) {
8 | var document = global.document = makeDocument("");
9 |
10 | try {
11 | d3.select('text').tspans([1, 2, 3])
12 | test.equal(d3.selectAll('tspan').size(), 3);
13 | test.equal(d3.select('text').text(), '123');
14 | test.end();
15 | } finally {
16 | delete global.document;
17 | }
18 | });
--------------------------------------------------------------------------------
/test/test-wordwrap.cjs:
--------------------------------------------------------------------------------
1 | var tape = require('tape'),
2 | jetpack = require('../build/d3-jetpack.cjs');
3 |
4 | tape('wordwrap second arg sets line length', function(test) {
5 | // test.equal(jetpack.wordwrap('two words', 4)[0], 'two');
6 | test.end();
7 | });
8 |
9 | tape('wordwrap default line length is 40', function(test) {
10 | // test.equal(jetpack.wordwrap('the default wrap length is 40 - this line will wrap')[1], 'wrap');
11 | test.end();
12 | });
13 |
14 | tape('no blank lines', function(test) {
15 | // test.equal(jetpack.wordwrap('thiswordistoolong', 5)[0], 'thiswordistoolong');
16 | test.end();
17 | });
18 |
19 |
--------------------------------------------------------------------------------