jupyter-ijavascript-utils 1.6.8 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS.md CHANGED
@@ -28,6 +28,8 @@ See the [#Installation section for requirements and installation](#install)
28
28
 
29
29
  ## What's New
30
30
 
31
+ * 1.8 - add in What can I Do tutorial, and object.join methods
32
+ * 1.7 - revamp of `animation` method for ijs.htmlScript
31
33
  * 1.6 - add SVG support for rendering SVGs and animations with {@link module:svg}.
32
34
  * 1.5 - Add LaTeX / KaTeX support with {@link module:latex} for rendering Math formulas and PlantUML support for Diagrams
33
35
  * 1.4 - Add in vega embed, vega mimetypes with {@link module:vega} and example choropleth tutorial
@@ -35,6 +37,22 @@ See the [#Installation section for requirements and installation](#install)
35
37
 
36
38
  -------
37
39
 
40
+ # What is this?
41
+
42
+ The [jupyter-ijavascript-utils](https://www.npmjs.com/package/jupyter-ijavascript-utils) library is simply a collection of utility methods for Node and JavaScript Developers interested in Data Science.
43
+
44
+ * Load
45
+ * Aggregate
46
+ * Manipulate
47
+ * Format / Visualize
48
+ * Refine and Explore
49
+
50
+ Currently, we assume you'll be using [nriesco's iJavaScript Jupyter Kernel](https://github.com/n-riesco/ijavascript) and the [Jupyter Lab - the latest interface for Jupyter](https://jupyter.org/) - and the installation is fairly simple in the [@link howToUse] guide. (Although suggestions welcome)
51
+
52
+ This is not intended to be the only way to accomplish many of these tasks, and alternatives are mentioned in the documentation as available.
53
+
54
+ ![Screenshot of example notebook](img/mainExampleNotebook.png)
55
+
38
56
  # For Example
39
57
 
40
58
  ## Get Sample Data
@@ -162,13 +180,18 @@ new utils.TableGenerator(barley)
162
180
  (See the {@tutorial vegaLite1} tutorial or the {@link module:vega|Vega module} for more)
163
181
 
164
182
  ```
183
+ //-- make a point chart
165
184
  utils.vega.svg((vl) => vl.markPoint()
185
+ //-- data as an array of items
166
186
  .data(barley)
167
187
  .title('Barley Yield by Site')
168
188
  .width(600)
169
189
  .encode(
190
+ //-- x position is Nominal - not a number
170
191
  vl.x().fieldN('site'),
192
+ //-- y position is Quantitative - a number
171
193
  vl.y().fieldQ('yield'),
194
+ //-- Color is based on the year field
172
195
  vl.color().fieldN('year')
173
196
  )
174
197
  )
@@ -176,7 +199,33 @@ utils.vega.svg((vl) => vl.markPoint()
176
199
 
177
200
  ![Screenshot of Vega Cell](img/BarleyYieldBySite.png)
178
201
 
179
- and more from {@link module:vega}
202
+ Where making it into a bar chart, to understand the proportions of varieties grown is simply changing the mark type
203
+
204
+ ```
205
+ // change from markPoint to markBar
206
+ utils.vega.svg((vl) => vl.markBar()
207
+ //-- data as an array of items
208
+ .data(barley)
209
+ .title('Barley Yield by Site Variety')
210
+ .width(600)
211
+ .encode(
212
+ //-- x position is Nominal - not a number
213
+ vl.x().fieldN('site').title('Site'),
214
+ //-- y position is Quantitative - a number
215
+ vl.y().fieldQ('yield').title('Yield'),
216
+ //-- Color is based on the variety field
217
+ vl.color().fieldN('variety').title('Variety')
218
+ )
219
+ )
220
+ ```
221
+
222
+ ![Screenshot of variety type](img/BarleyYieldBySiteVariety.png)
223
+
224
+ With further options to zoom, pan, or setup interactive sliders:
225
+
226
+ ![Screenshot of vega lite with sliders](img/vegaLiteSliders.png)
227
+
228
+ Or try your hand at the [Vega Lite Examples](https://vega.github.io/vega-lite/examples/) and more from {@link module:vega}
180
229
 
181
230
  ![Screenshot of Vega-Lite Examples](img/vegaLiteExamples.png)
182
231
 
package/README.md CHANGED
@@ -18,11 +18,29 @@ See documentation at: [https://jupyter-ijavascript-utils.onrender.com/](https://
18
18
 
19
19
  # What's New
20
20
 
21
+ * 1.8 - add in What can I Do tutorial, and object.join methods
22
+ * 1.7 - revamp of `animation` method to htmlScript
21
23
  * 1.6 - add SVG support for rendering SVGs and animations
22
24
  * 1.5 - Add Latex support for rendering Math formulas and PlantUML support for Diagrams
23
25
  * 1.4 - Add in vega embed, vega mimetypes and example choropleth tutorial
24
26
  * 1.3 - Add Leaflet for Maps, allow Vega to use explicit specs (so [Examples can be copied and pasted](https://vega.github.io/vega-lite/examples/), and add in htmlScripts
25
27
 
28
+ # What is this?
29
+
30
+ The [jupyter-ijavascript-utils](https://www.npmjs.com/package/jupyter-ijavascript-utils) library is simply a collection of utility methods for Node and JavaScript Developers interested in Data Science.
31
+
32
+ * Load
33
+ * Aggregate
34
+ * Manipulate
35
+ * Format / Visualize
36
+ * Refine and Explore
37
+
38
+ Currently, we assume you'll be using [nriesco's iJavaScript Jupyter Kernel](https://github.com/n-riesco/ijavascript) and the [Jupyter Lab - the latest interface for Jupyter](https://jupyter.org/) - and the installation is fairly simple in the [How to Use guide](https://jupyter-ijavascript-utils.onrender.com/tutorial-howToUse.html). (Although suggestions welcome)
39
+
40
+ This is not intended to be the only way to accomplish many of these tasks, and alternatives are mentioned in the documentation as available.
41
+
42
+ ![Screenshot of example notebook](https://jupyter-ijavascript-utils.onrender.com/img/mainExampleNotebook.png)
43
+
26
44
  # For Example
27
45
 
28
46
  ## Get Sample Data
@@ -147,16 +165,21 @@ new utils.TableGenerator(barley)
147
165
 
148
166
  ## Show a Graph
149
167
 
150
- ([See the vega module for more](https://jupyter-ijavascript-utils.onrender.com/module-vega.html))
168
+ (See the {@tutorial vegaLite1} tutorial or the {@link module:vega|Vega module} for more)
151
169
 
152
170
  ```
171
+ //-- make a point chart
153
172
  utils.vega.svg((vl) => vl.markPoint()
173
+ //-- data as an array of items
154
174
  .data(barley)
155
175
  .title('Barley Yield by Site')
156
176
  .width(600)
157
177
  .encode(
178
+ //-- x position is Nominal - not a number
158
179
  vl.x().fieldN('site'),
180
+ //-- y position is Quantitative - a number
159
181
  vl.y().fieldQ('yield'),
182
+ //-- Color is based on the year field
160
183
  vl.color().fieldN('year')
161
184
  )
162
185
  )
@@ -164,6 +187,32 @@ utils.vega.svg((vl) => vl.markPoint()
164
187
 
165
188
  ![Screenshot of Vega Cell](https://jupyter-ijavascript-utils.onrender.com/img/BarleyYieldBySite.png)
166
189
 
190
+ Where making it into a bar chart, to understand the proportions of varieties grown is simply changing the mark type
191
+
192
+ ```
193
+ // change from markPoint to markBar
194
+ utils.vega.svg((vl) => vl.markBar()
195
+ //-- data as an array of items
196
+ .data(barley)
197
+ .title('Barley Yield by Site Variety')
198
+ .width(600)
199
+ .encode(
200
+ //-- x position is Nominal - not a number
201
+ vl.x().fieldN('site').title('Site'),
202
+ //-- y position is Quantitative - a number
203
+ vl.y().fieldQ('yield').title('Yield'),
204
+ //-- Color is based on the variety field
205
+ vl.color().fieldN('variety').title('Variety')
206
+ )
207
+ )
208
+ ```
209
+
210
+ ![Screenshot of variety type](https://jupyter-ijavascript-utils.onrender.com/img/BarleyYieldBySiteVariety.png)
211
+
212
+ With further options to zoom, pan, or setup interactive sliders:
213
+
214
+ ![Screenshot of vega lite with sliders](https://jupyter-ijavascript-utils.onrender.com/img/vegaLiteSliders.png)
215
+
167
216
  Or try your hand at the [Vega Lite Examples](https://vega.github.io/vega-lite/examples/)
168
217
 
169
218
  ![Screenshot of Vega-Lite Examples](https://jupyter-ijavascript-utils.onrender.com/img/vegaLiteExamplesSm.png)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.6.8",
3
+ "version": "1.8.0",
4
4
  "description": "Utilities for working with iJavaScript - a Jupyter Kernel",
5
5
  "homepage": "https://jupyter-ijavascript-utils.onrender.com/",
6
6
  "license": "MIT",
package/src/aggregate.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable implicit-arrow-linebreak */
2
2
 
3
+ const ObjectUtils = require('./object');
3
4
  const FormatUtils = require('./format');
4
5
 
5
6
  /**
@@ -272,24 +273,6 @@ module.exports.deferCollection = (aggregateFn, ...rest) => {
272
273
 
273
274
  module.exports.defer = module.exports.deferCollection;
274
275
 
275
- /**
276
- * Generates a function if a property or null or a function is sent
277
- * @param {Function | String} fnOrProp -
278
- * @return {Function}
279
- * @private
280
- */
281
- module.exports.evaluateFunctionOrProperty = function evaluateFunctionOrProperty(fnOrProp) {
282
- if (!fnOrProp) {
283
- return (r) => r;
284
- } else if (typeof fnOrProp === 'string') {
285
- return (r) => r[fnOrProp];
286
- } else if (typeof fnOrProp === 'function') {
287
- return fnOrProp;
288
- }
289
-
290
- throw (Error('Send either a Function or Property Name or null for a simple array'));
291
- };
292
-
293
276
  /**
294
277
  * Identifies the min and max of values of a collection
295
278
  * @param {Array} collection -
@@ -318,7 +301,7 @@ module.exports.extent = function extent(collection, accessor) {
318
301
  * // 0.87
319
302
  */
320
303
  module.exports.min = function min(collection, accessor) {
321
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
304
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
322
305
  return collection.reduce((current, val) => {
323
306
  const valEval = cleanedFunc(val);
324
307
  return valEval < current ? valEval : current;
@@ -338,7 +321,7 @@ module.exports.min = function min(collection, accessor) {
338
321
  * // 5.31
339
322
  */
340
323
  module.exports.max = function max(collection, accessor) {
341
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
324
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
342
325
  return collection.reduce((current, val) => {
343
326
  const valEval = cleanedFunc(val);
344
327
  return valEval > current ? valEval : current;
@@ -356,7 +339,7 @@ module.exports.max = function max(collection, accessor) {
356
339
  * // 26.69
357
340
  */
358
341
  module.exports.sum = function sum(collection, accessor) {
359
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
342
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
360
343
  return collection.reduce((current, val) => current + cleanedFunc(val), 0);
361
344
  };
362
345
 
@@ -385,7 +368,7 @@ module.exports.difference = function difference(collection, accessor) {
385
368
  * // 3.41
386
369
  */
387
370
  module.exports.avgMean = function avgMean(collection, accessor) {
388
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
371
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
389
372
  return collection.reduce((current, val) => current + cleanedFunc(val), 0)
390
373
  / collection.length;
391
374
  };
@@ -401,7 +384,7 @@ module.exports.avgMean = function avgMean(collection, accessor) {
401
384
  * // 3.62
402
385
  */
403
386
  module.exports.avgMedian = function avgMedian(collection, accessor) {
404
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
387
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
405
388
  const results = collection.map(cleanedFunc).sort((a, b) => a - b);
406
389
  const middle = Math.floor(collection.length / 2);
407
390
  return collection.length % 2 === 0
@@ -422,7 +405,7 @@ module.exports.avgMedian = function avgMedian(collection, accessor) {
422
405
  * // 0.87
423
406
  */
424
407
  module.exports.first = function first(collection, accessor) {
425
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
408
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
426
409
  let result = null;
427
410
  for (let i = 0; i < collection.length; i += 1) {
428
411
  result = cleanedFunc(collection[i]);
@@ -464,7 +447,7 @@ module.exports.length = function length(collection) {
464
447
  * // [ 'apple', 'orange', 'banana' ]
465
448
  */
466
449
  module.exports.unique = function unique(collection, accessor, uniquifierFn) {
467
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
450
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
468
451
  if (uniquifierFn) {
469
452
  return Array.from(new Set(
470
453
  collection.map((v) => uniquifierFn(cleanedFunc(v)))
@@ -525,7 +508,7 @@ module.exports.distinct = function distinct(collection, accessor, uniquifierFn)
525
508
  * // 2
526
509
  */
527
510
  module.exports.countMap = function countMap(collection, accessor, uniquifierFn) {
528
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
511
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
529
512
  const resultMap = new Map();
530
513
  collection.forEach((val) => {
531
514
  let result = cleanedFunc(val);
@@ -636,7 +619,7 @@ module.exports.duplicates = function duplicates(collection, accessor, uniquifier
636
619
  * // Set('d')
637
620
  */
638
621
  module.exports.notIn = function notIn(collection, accessor, targetIterator) {
639
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
622
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
640
623
  const targetSet = new Set(targetIterator);
641
624
  const results = new Set();
642
625
  collection.forEach((record) => {
@@ -665,7 +648,7 @@ module.exports.notIn = function notIn(collection, accessor, targetIterator) {
665
648
  * aggregate.isUnique(data); // true
666
649
  */
667
650
  module.exports.isUnique = function isUnique(collection, accessor) {
668
- const cleanedFunc = AggregateUtils.evaluateFunctionOrProperty(accessor);
651
+ const cleanedFunc = ObjectUtils.evaluateFunctionOrProperty(accessor);
669
652
  const uniqueValues = new Set();
670
653
  const duplicateValue = collection.find((record) => {
671
654
  const result = cleanedFunc(record);
package/src/group.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const SourceMap = require('./SourceMap');
2
2
 
3
- const AggregateUtils = require('./aggregate');
3
+ const ObjectUtils = require('./object');
4
4
 
5
5
  /**
6
6
  * Utilities for collating and grouping records
@@ -308,7 +308,7 @@ module.exports.index = function index(collection, indexFn) {
308
308
  throw (Error('group.index: Collection is not an array'));
309
309
  }
310
310
 
311
- const cleanedIndexFn = AggregateUtils.evaluateFunctionOrProperty(indexFn);
311
+ const cleanedIndexFn = ObjectUtils.evaluateFunctionOrProperty(indexFn);
312
312
 
313
313
  collection.forEach((item, offset) => {
314
314
  let val = cleanedIndexFn(item, offset);
package/src/ijs.js CHANGED
@@ -363,6 +363,9 @@ module.exports.listStatic = function listStatic(target) {
363
363
  * @param {String | Function} options.onReady - JavaScript to run once all files have loaded
364
364
  * @param {Element} options.onReady.rootEl - results div container
365
365
  * @param {any} [options.onReady.data] - the options.data parameter
366
+ * @param {Object} options.onReady.utilityFunctions - the options.utilityFunctions object
367
+ * @param {Object} options.onReady.options - the options object passed
368
+ * @param {Object} options.onReady.animate - alias to requestAnimationFrame with additional checks to avoid leaks
366
369
  * @param {String[]} [options.scripts = []] - Array of JavaScript file addresses to load
367
370
  * @param {String[]} [options.css = []] - Array of CSS file addresses to load
368
371
  * @param {any} [options.data = undefined] - any nodejs data you would like available in javaScript
@@ -450,7 +453,7 @@ module.exports.htmlScript = function htmlScripts(
450
453
  if (!onReadyCode) {
451
454
  throw Error('ijsUtils.htmlScript: onReadyCode is required');
452
455
  } else if (typeof onReadyCode === 'function') {
453
- onReadyCode = `(${onReadyCode.toString()})({rootEl, data, utilityFunctions, options})`;
456
+ onReadyCode = `(${onReadyCode.toString()})({rootEl, data, utilityFunctions, options, animate})`;
454
457
  } else if (typeof onReadyCode === 'string') {
455
458
  onReadyCode = onReadyCode.trim();
456
459
  if (!onReadyCode.endsWith(';')) {
@@ -509,6 +512,16 @@ module.exports.htmlScript = function htmlScripts(
509
512
  css: ${JSON.stringify(scriptAddresses)},
510
513
  };
511
514
 
515
+ const animate = function (requestAnimationFrameTarget) {
516
+ requestAnimationFrame((...passThroughArgs) => {
517
+ if (!document.contains(rootEl)) {
518
+ console.log('old animation stopping. rootEl has been removed from DOM');
519
+ return;
520
+ }
521
+ requestAnimationFrameTarget.apply(globalThis, passThroughArgs);
522
+ })
523
+ }
524
+
512
525
  //-- ijsUtils.htmlScipt options.data
513
526
  const data = ${JSON.stringify(data)};
514
527
 
package/src/object.js CHANGED
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-param-reassign */
1
+ /* eslint-disable no-param-reassign, max-len */
2
2
 
3
3
  const schemaGenerator = require('generate-schema');
4
4
 
@@ -14,15 +14,17 @@ const schemaGenerator = require('generate-schema');
14
14
  * * {@link module:object.objAssignEntities|objAssignEntities()} -
15
15
  * * {@link module:object.selectObjectProperties|selectObjectProperties()} - keep only specific properties
16
16
  * * {@link module:object.filterObjectProperties|filterObjectProperties()} - remove specific properties
17
- * * fetch child properties onto parents
18
- * * {@link module:object.fetchObjectProperties|fetchObjectProperties()} - use dot notation to bring multiple child properties onto a parent
19
- * * {@link module:object.fetchObjectProperty|fetchObjectProperty()} - use dot notation to bring a child property onto a parent
17
+ * * Fetch child properties from related objects
18
+ * * {@link module:object.fetchObjectProperty|fetchObjectProperty(object, string)} - use dot notation to bring a child property onto a parent
19
+ * * {@link module:object.fetchObjectProperties|fetchObjectProperties(object, string[])} - use dot notation to bring multiple child properties onto a parent
20
+ * * {@link module:object.join|join(array, index, map, fn)} - join a collection against a map by a given index
21
+ * * {@link module:object.joinProperties|join(array, index, map, ...fields)} - join a collection, and copy properties over from the mapped object.
20
22
  * * Rename properties
21
23
  * * {@link module:object.cleanProperties|cleanProperties()} - correct inaccessible property names in a list of objects
22
24
  * * {@link module:object.cleanPropertyNames|cleanPropertyNames()} - create a translation of inaccessible names to accessible ones
23
25
  * * {@link module:object.cleanPropertyName|cleanPropertyName()} - create a translation of a specific property name to be accessible.
24
26
  * * {@link module:object.renameProperties|renameProperties()} - Use a translation from old property names to new ones
25
- * * flatten object properties
27
+ * * Flatten object properties
26
28
  * * {@link module:object.collapseSpecificObject|collapseSpecificObject()} - flatten object properties
27
29
  * * {@link module:object.collapse|collapse()} - flatten specific object
28
30
  * * Create Map of objects by key
@@ -38,6 +40,30 @@ const ObjectUtils = module.exports;
38
40
 
39
41
  //-- private methods
40
42
 
43
+ /**
44
+ * Generates a function if a property or null or a function is sent
45
+ * @param {Function | String} fnOrProp -
46
+ * @return {Function}
47
+ * @private
48
+ */
49
+ module.exports.evaluateFunctionOrProperty = function evaluateFunctionOrProperty(fnOrProp) {
50
+ if (!fnOrProp) {
51
+ return (r) => r;
52
+ } else if (typeof fnOrProp === 'string') {
53
+ return (r) => r[fnOrProp];
54
+ } else if (typeof fnOrProp === 'function') {
55
+ return fnOrProp;
56
+ }
57
+
58
+ throw (Error('Send either a Function or Property Name or null for a simple array'));
59
+ };
60
+
61
+ /**
62
+ * Identifies keys from an object, but handles null safely.
63
+ * @param {Object} - object to get the keys from
64
+ * @return {Array<String>} - collections of keys or [] if no keys are found.
65
+ * @private
66
+ */
41
67
  const keysFromObject = (obj) => {
42
68
  if (!obj) {
43
69
  return [];
@@ -353,7 +379,7 @@ module.exports.filterObjectProperties = function filterObjectProperties(list, pr
353
379
  * @param {Map<String,any>} propertyNames - Object with the keys as the properties
354
380
  * and the values using dot notation to access related records and properties
355
381
  * (ex: {parentName: 'somePropertyObject.parent.parent.name', childName: 'child.Name'})
356
- * @param {FetchObjectOptions} options -
382
+ * @param {FetchObjectOptions} options - {@link module:object~FetchObjectOptions|See FetchObjectOptions}
357
383
  * @returns {Object[]} - objects with the properties resolved
358
384
  * (ex: {parentname, childName, etc.})
359
385
  */
@@ -385,7 +411,7 @@ module.exports.fetchObjectProperties = function fetchObjectProperties(list, prop
385
411
  * @param {Object} obj - object to access the properties on
386
412
  * @param {String} propertyAccess - dot notation for the property to access
387
413
  * (ex: `parent.obj.Name`)
388
- * @param {FetchObjectOptions} options -
414
+ * @param {FetchObjectOptions} options - {@link module:object~FetchObjectOptions|See FetchObjectOptions}
389
415
  * @returns {any} - the value accessed at the end ofthe property chain
390
416
  */
391
417
  module.exports.fetchObjectProperty = function fetchObjectProperty(obj, propertyAccess, safeAccess) {
@@ -441,3 +467,170 @@ module.exports.getObjectPropertyTypes = function getObjectPropertyTypes(list) {
441
467
  module.exports.generateSchema = function generateSchema(targetObj) {
442
468
  return schemaGenerator.json(targetObj);
443
469
  };
470
+
471
+ /**
472
+ * Join values from an objectArray to a JavaScript Map.
473
+ *
474
+ * For example:
475
+ *
476
+ * ```
477
+ * weather = [
478
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
479
+ * null,
480
+ * { id: 3, city: 'New York', month: 'Apr', precip: 3.94 },
481
+ * { id: 6, city: 'Chicago', month: 'Apr', precip: 3.62 }
482
+ * ];
483
+ *
484
+ * cityLocations = new Map([
485
+ * ['Chicago', { locationId: 1, city: 'Chicago', lat: 41.8781, lon: 87.6298 }],
486
+ * ['New York', { locationId: 2, city: 'New York', lat: 40.7128, lon: 74.0060 }],
487
+ * ['Seattle', { locationId: 3, city: 'Seattle', lat: 47.6062, lon: 122.3321 }]
488
+ * ]);
489
+ *
490
+ * utils.object.join(weather, 'city', cityLocations, (weather, city) => ({...weather, ...city}));
491
+ * // [
492
+ * // {id:1, city:'Seattle', month:'Aug', precip:0.87, locationId:3, lat:47.6062, lon:122.3321 },
493
+ * // null,
494
+ * // {id:3, city:'New York', month:'Apr', precip:3.94, locationId:2, lat:40.7128, lon:74.006 },
495
+ * // {id:6, city:'Chicago', month:'Apr', precip:3.62, locationId:1, lat:41.8781, lon:87.6298 }
496
+ * // ]
497
+ * ```
498
+ *
499
+ * or join by lookup:
500
+ *
501
+ * ```
502
+ * utils.object.join(weather, 'city', cityLocations, (weather, city) => ({...weather, city}));
503
+ * [
504
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87, city:
505
+ * { city: 'Seattle', locationId: 3, lat: 47.6062, lon: 122.3321 }
506
+ * },
507
+ * null,
508
+ * { id: 3, city: 'New York', month: 'Apr', precip: 3.94, city:
509
+ * { city: 'New York', locationId: 2, lat: 40.7128, lon: 74.006 }
510
+ * },
511
+ * { id: 6, city: 'Chicago', month: 'Apr', precip: 3.62, city:
512
+ * { city: 'Chicago', locationId: 1, lat: 41.8781, lon: 87.6298 }
513
+ * }
514
+ * ];
515
+ * ```
516
+ *
517
+ * or performing a translation / calculate the index instead of a property:
518
+ *
519
+ * ```
520
+ * const indexingFn = (weather) => `${weather.country}_${weather.city}`;
521
+ * utils.object.join(weather, indexingFn, cityLocations, (weather, city) => ({...weather, ...city}));
522
+ * // ...
523
+ * ```
524
+ *
525
+ * The signature for the indexingFunction is `(sourceObj:Object): {any}` - providing the index to use against the map.
526
+ *
527
+ * The signature for the mapping function is `(sourceObj:Object, mappedObject:Object) => {Object}`.
528
+ *
529
+ * If the mappedObject could not be found by that index (left join), then mappedObject will be `null`.
530
+ *
531
+ * As the results of the functions are mapped, you can either modify in-line (directly on the object),
532
+ * or on a clone of the object (ex: {...sourceObj})
533
+ *
534
+ * Note, performing a JavaScript .map() call may be more performant in some cases,
535
+ * so consider it for more complex options.
536
+ *
537
+ * **Note: indexField can be either a string name of the field to join,
538
+ * or a function to be passed the object and generate the index**
539
+ *
540
+ * @param {Array<Object>} objectArray - collection of objects to join based on the target map
541
+ * @param {Function | String} indexField - property on each object in array to lookup against target map <br />
542
+ * Signature if a function: `(sourceObj:Object): {any}`
543
+ * @param {Map} targetMap - Map with keys mapping to values to pass
544
+ * @param {Function} joinFn - function to call each time an objectArray object, has an indexField found in targetMap <br />
545
+ * Signature: `(sourceObj:Object, mappedObject:Object) => {Object}`
546
+ * @returns {Array<Object>} - Array of results returned from `joinFn`
547
+ */
548
+ module.exports.join = function join(objectArray, indexField, targetMap, joinFn) {
549
+ const cleanArray = !objectArray
550
+ ? []
551
+ : Array.isArray(objectArray)
552
+ ? objectArray
553
+ : [objectArray];
554
+
555
+ const indexFn = ObjectUtils.evaluateFunctionOrProperty(indexField);
556
+
557
+ if (!targetMap) {
558
+ throw Error('object.join(objectArray, indexField, targetMap, joinFn): targetMap cannot be null');
559
+ }
560
+
561
+ if (!joinFn || typeof joinFn !== 'function') {
562
+ throw Error('object.join(objectArray, indexField, targetMap, joinFn): joinFn is required');
563
+ }
564
+
565
+ const results = cleanArray.map((entry) => {
566
+ if (!entry) return entry;
567
+
568
+ const index = indexFn(entry);
569
+
570
+ const target = targetMap.has(index)
571
+ ? targetMap.get(index)
572
+ : null;
573
+
574
+ const result = joinFn(entry, target);
575
+
576
+ return result;
577
+ });
578
+
579
+ return results;
580
+ };
581
+
582
+ /**
583
+ * For cases where we simply want to pull values from one object to another.
584
+ *
585
+ * For example:
586
+ *
587
+ * ```
588
+ * weather = [
589
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
590
+ * null,
591
+ * { id: 3, city: 'New York', month: 'Apr', precip: 3.94 },
592
+ * { id: 6, city: 'Chicago', month: 'Apr', precip: 3.62 }
593
+ * ];
594
+ *
595
+ * cityLocations = new Map([
596
+ * ['Chicago', { locationId: 1, city: 'Chicago', lat: 41.8781, lon: 87.6298 }],
597
+ * ['New York', { locationId: 2, city: 'New York', lat: 40.7128, lon: 74.0060 }],
598
+ * ['Seattle', { locationId: 3, city: 'Seattle', lat: 47.6062, lon: 122.3321 }]
599
+ * ]);
600
+ *
601
+ * utils.object.joinProperties(weather, 'city', cityLocations, 'lat', 'lon'));
602
+ * // [
603
+ * // {id:1, city:'Seattle', month:'Aug', precip:0.87, lat:47.6062, lon:122.3321 },
604
+ * // null,
605
+ * // {id:3, city:'New York', month:'Apr', precip:3.94, lat:40.7128, lon:74.006 },
606
+ * // {id:6, city:'Chicago', month:'Apr', precip:3.62, lat:41.8781, lon:87.6298 }
607
+ * // ]
608
+ * ```
609
+ *
610
+ * @param {Array<Object>} objectArray - collection of objects to join based on the target map
611
+ * @param {Function | String} indexField - property on each object in array to lookup against target map <br />
612
+ * Signature if a function: `(sourceObj:Object): {any}`
613
+ * @param {Map<any,Object>} targetMap - Map with keys mapping to values to pass
614
+ * @param {...String} fields - List of fields to add to the objectArray in-place against values from targetMap
615
+ * @returns {Array<Object>} - The modified objectArray with the fields applied.
616
+ */
617
+ module.exports.joinProperties = function join(objectArray, indexField, targetMap, ...fields) {
618
+ const cleanFields = fields.filter((f) => f);
619
+ if (cleanFields.length < 1) {
620
+ throw Error('object.joinProperties(objectArray, indexField, targetMap, ...fields): at least one property passed to join');
621
+ }
622
+
623
+ const joinFn = (sourceObj, targetObj) => {
624
+ const cleanTarget = targetObj || {};
625
+ //-- allow for direct manipulation for speed
626
+ const result = sourceObj; // { ...sourceObj };
627
+
628
+ cleanFields.forEach((field) => {
629
+ result[field] = cleanTarget[field];
630
+ });
631
+
632
+ return result;
633
+ };
634
+
635
+ return ObjectUtils.join(objectArray, indexField, targetMap, joinFn);
636
+ };
package/src/svg.js CHANGED
@@ -318,11 +318,13 @@ Renders an SVG through a browser side instance of [SVG.js](https://svgjs.dev/)
318
318
  * @param {Object} options - options to use for drawing - or an onReady function
319
319
  * @param {Function} options.onReady - the function to call to generate the SVG
320
320
  * @param {Element} options.onReady.el - the SVG.js primed element to use for drawing
321
+ * @param {Object} options.onReady.data - the data object passed - now in javascript
321
322
  * @param {any} options.onReady.SVG - the SVG.js library instance
322
323
  * @param {Number} options.onReady.width - the options.width value passed, for positioning
323
324
  * @param {Number} options.onReady.height - the options.height value passed, for positioning
324
325
  * @param {Object} options.onReady.utilityFunctions - the options.utilityFunctions object
325
326
  * @param {Object} options.onReady.options - the options object passed
327
+ * @param {Object} options.onReady.animate - alias to requestAnimationFrame with additional checks to avoid leaks
326
328
  * @param {boolean} options.debug - default: false - whether to print the svg result text
327
329
  * @param {Number} options.width - default: 400 - the width of the svg to generate
328
330
  * @param {Number} options.height - default 200 - ... height
@@ -366,7 +368,7 @@ const width = ${width};
366
368
  const height = ${height};
367
369
  el.size(width, height);
368
370
 
369
- (${onReady.toString()})({ rootEl, el, SVG, data, width, height, utilityFunctions, options });
371
+ (${onReady.toString()})({ rootEl, el, SVG, data, width, height, utilityFunctions, options, animate });
370
372
  `
371
373
  });
372
374
  };
@@ -99,8 +99,7 @@ const SvgUtils = module.exports; // eslint-disable-line no-unused-vars
99
99
  module.exports.animationFrameCalls = function animationFrameCalls() {
100
100
  const requestAnimationFrame = window.requestAnimationFrame
101
101
  || window.mozRequestAnimationFrame
102
- || window.webkitRequestAnimationFrame
103
- || window.msRequestAnimationFrame;
102
+ || window.webkitRequestAnimationFrame;
104
103
 
105
104
  const cancelAnimationFrame = window.cancelAnimationFrame
106
105
  || window.mozCancelAnimationFrame;
@@ -123,8 +122,15 @@ module.exports.animationFrameCalls = function animationFrameCalls() {
123
122
  window.stopAnimation = isAllowed;
124
123
  };
125
124
 
126
- const nextAnimationFrame = (fn) => {
127
- const animationId = requestAnimationFrame(fn);
125
+ const nextAnimationFrame = (fn, el) => {
126
+ const requestFn = (...rest) => {
127
+ if (el && !window.document.contains(el)) {
128
+ console.log('old nextAnimationFrame aborting, el has been removed from DOM');
129
+ } else {
130
+ fn.apply(globalThis, rest);
131
+ }
132
+ };
133
+ const animationId = requestAnimationFrame(requestFn);
128
134
  window.animation = animationId;
129
135
  };
130
136
 
@@ -146,3 +152,5 @@ module.exports.animationFrameCalls = function animationFrameCalls() {
146
152
  allowAnimations
147
153
  };
148
154
  };
155
+
156
+ module.exports.animation = module.exports.animationFrameCalls;