tape-six 0.9.0 → 0.9.1

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/README.md CHANGED
@@ -1,9 +1,17 @@
1
- # tape6 [![NPM version][npm-img]][npm-url]
1
+ # tape-six [![NPM version][npm-img]][npm-url]
2
2
 
3
- [npm-img]: https://img.shields.io/npm/v/tape6.svg
4
- [npm-url]: https://npmjs.org/package/tape6
3
+ [npm-img]: https://img.shields.io/npm/v/tape-six.svg
4
+ [npm-url]: https://npmjs.org/package/tape-six
5
5
 
6
- tape6 is a [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol)-based library for unit tests. It is written in modern ES6 and works in [node](https://nodejs.org/), [deno](https://deno.land/) and browsers.
6
+ tape-six is a [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol)-based library for unit tests. It is written in modern ES6 and works in [node](https://nodejs.org/), [deno](https://deno.land/) and browsers.
7
+
8
+ Why `tape-six`? It was supposed to be named `tape6` but `npm` does not allow names "similar" to existing packages. Instead of eliminating name-squatting they force to use unintuitive and unmemorable names. That's why all internal names, environment variables, and public names still use `tape6`.
9
+
10
+ Why another library? Working on projects written in modern JS (with modules) I found two problems with existing unit test libraries:
11
+
12
+ * In my opinion unit test files should be directly executable with `node`, `deno`, browsers (with a trivial HTML file to load a test file) without a need for a special test runner utility, which wraps and massages my beautiful code.
13
+ * Some of them do not work with modules.
14
+ * Some of them have the abysmal developer experience in browsers.
7
15
 
8
16
  The documentation is TBD but you can inspect `tests/` to see how it is used.
9
17
  If you are familiar with other TAP-based libraries you'll feel right at home.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "TAP for the modern JavaScript (ES6).",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -19,10 +19,10 @@
19
19
  "build": "npm run copyDeep6",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
- "github": "http://github.com/uhop/tape6",
22
+ "github": "http://github.com/uhop/tape-six",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/uhop/tape6.git"
25
+ "url": "git+https://github.com/uhop/tape-six.git"
26
26
  },
27
27
  "keywords": [
28
28
  "tap",
@@ -34,15 +34,14 @@
34
34
  "author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
35
35
  "license": "BSD-3-Clause",
36
36
  "bugs": {
37
- "url": "https://github.com/uhop/tape6/issues"
37
+ "url": "https://github.com/uhop/tape-six/issues"
38
38
  },
39
- "homepage": "https://github.com/uhop/tape6#readme",
39
+ "homepage": "https://github.com/uhop/tape-six#readme",
40
40
  "files": [
41
41
  "index.js",
42
42
  "bin",
43
- "web",
44
- "src",
45
- "cjs"
43
+ "webApp",
44
+ "src"
46
45
  ],
47
46
  "tape6": {
48
47
  "tests": [
@@ -0,0 +1,114 @@
1
+ import {formatNumber} from '../src/utils/formatters.js';
2
+
3
+ const tagsToReplace = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
4
+ const escapeHtml = string => string.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag);
5
+
6
+ const formatName = event =>
7
+ '<span class="' +
8
+ (event.fail ? 'text-failure' : 'text-success') +
9
+ '">' +
10
+ ((event.skip && '<span class="text-skipped">SKIP</span>&nbsp;') || '') +
11
+ ((event.todo && '<span class="text-todo">TODO</span>&nbsp;') || '') +
12
+ escapeHtml(event.name || '') +
13
+ '</span>';
14
+
15
+ class DashReporter {
16
+ constructor({renumberAsserts = false} = {}) {
17
+ this.renumberAsserts = renumberAsserts;
18
+ this.assertCounter = 0;
19
+ this.depth = this.assertCounter = this.failureCounter = this.skipCounter = this.todoCounter = 0;
20
+ this.testCounter = -1;
21
+ this.currentTest = this.lastAssert = '';
22
+ this.scoreNode = document.querySelector('.tape6 .score');
23
+ this.donutNode = this.scoreNode && this.scoreNode.querySelector('tape6-donut');
24
+ this.legendNode = document.querySelector('.tape6 .legend');
25
+ this.spinnerNode = document.querySelector('.tape6 tape6-spinner');
26
+ this.running = true;
27
+ }
28
+ report(event) {
29
+ switch (event.type) {
30
+ case 'test':
31
+ ++this.depth;
32
+ ++this.testCounter;
33
+ this.currentTest = formatName(event);
34
+ this.updateDashboard();
35
+ break;
36
+ case 'end':
37
+ if (!--this.depth) {
38
+ this.running = false;
39
+ this.updateDashboard();
40
+ }
41
+ break;
42
+ case 'assert':
43
+ ++this.assertCounter;
44
+ event.fail && !event.skip && !event.todo && ++this.failureCounter;
45
+ event.skip && ++this.skipCounter;
46
+ event.todo && ++this.todoCounter;
47
+ this.lastAssert = formatName(event);
48
+ this.updateDashboard();
49
+ break;
50
+ }
51
+ }
52
+ updateDashboard() {
53
+ this.updateDonut();
54
+ this.updateLegend();
55
+ this.updateStatus();
56
+ this.updateScoreCard();
57
+ }
58
+ updateDonut() {
59
+ if (!this.donutNode) return;
60
+ const total = this.assertCounter - this.skipCounter,
61
+ success = total - this.failureCounter;
62
+ this.donutNode.show([
63
+ {value: success, className: 'success'},
64
+ {value: this.failureCounter, className: 'failure'},
65
+ {value: this.skipCounter, className: 'skipped'},
66
+ {value: this.todoCounter, className: 'todo'}
67
+ ]);
68
+ }
69
+ updateLegend() {
70
+ if (!this.legendNode) return;
71
+ const total = this.assertCounter - this.skipCounter,
72
+ success = total - this.failureCounter;
73
+ let node = this.legendNode.querySelector('.legend-tests .value');
74
+ node && (node.innerHTML = formatNumber(this.testCounter));
75
+ node = this.legendNode.querySelector('.legend-asserts .value');
76
+ node && (node.innerHTML = formatNumber(this.assertCounter));
77
+ node = this.legendNode.querySelector('.legend-success .value');
78
+ node && (node.innerHTML = formatNumber(success));
79
+ node = this.legendNode.querySelector('.legend-failure .value');
80
+ node && (node.innerHTML = formatNumber(this.failureCounter));
81
+ node = this.legendNode.querySelector('.legend-skipped .value');
82
+ node && (node.innerHTML = formatNumber(this.skipCounter));
83
+ node = this.legendNode.querySelector('.legend-todo .value');
84
+ node && (node.innerHTML = formatNumber(this.todoCounter));
85
+ }
86
+ updateStatus() {
87
+ if (!this.spinnerNode) return;
88
+ this.spinnerNode[this.running ? 'show' : 'hide']();
89
+ }
90
+ updateScoreCard() {
91
+ if (!this.scoreNode) return;
92
+ const total = this.assertCounter - this.skipCounter,
93
+ success = total - this.failureCounter,
94
+ fail = success < total,
95
+ result = total > 0 ? formatNumber(100 * (success / total), 1) : '100';
96
+ let node = this.scoreNode.querySelector('.text');
97
+ node.classList.remove('nothing');
98
+ if (fail) {
99
+ node.classList.remove('success-dark');
100
+ node.classList.add('failure-dark');
101
+ } else {
102
+ node.classList.remove('failure-dark');
103
+ node.classList.add('success-dark');
104
+ }
105
+ node = this.scoreNode.querySelector('.message');
106
+ while (node.lastChild) node.removeChild(node.lastChild);
107
+ node.appendChild(document.createTextNode(fail ? 'Need some work' : 'All good!'));
108
+ node = this.scoreNode.querySelector('.result');
109
+ while (node.lastChild) node.removeChild(node.lastChild);
110
+ node.appendChild(document.createTextNode(result + '% passed'));
111
+ }
112
+ }
113
+
114
+ export default DashReporter;
@@ -0,0 +1,160 @@
1
+ const formatValue = value => {
2
+ if (typeof value == 'string') return value;
3
+ return JSON.stringify(value);
4
+ };
5
+
6
+ class DomReporter {
7
+ constructor({root, renumberAsserts = false} = {}) {
8
+ this.root = root;
9
+ this.renumberAsserts = renumberAsserts;
10
+ this.assertCounter = 0;
11
+ this.stack = []; // previous test nodes
12
+ this.current = null; // the current test node
13
+ }
14
+ report(event) {
15
+ let text;
16
+ switch (event.type) {
17
+ case 'test':
18
+ this.stack.push(this.current);
19
+ if (this.stack.length <= 1) break;
20
+ this.current = document.createElement('div');
21
+ this.current.className = 'test running';
22
+ {
23
+ const header = document.createElement('div');
24
+ header.className = 'header';
25
+ if (event.todo) {
26
+ const todo = document.createElement('span');
27
+ todo.className = 'text-todo';
28
+ todo.appendChild(document.createTextNode('TODO'));
29
+ header.appendChild(todo);
30
+ header.appendChild(document.createTextNode(' '));
31
+ }
32
+ header.appendChild(document.createTextNode(event.name));
33
+ this.current.appendChild(header);
34
+ }
35
+ ((this.stack.length && this.stack[this.stack.length - 1]) || this.root).appendChild(this.current);
36
+ break;
37
+ case 'end':
38
+ if (this.current) {
39
+ this.current.classList.remove('running');
40
+ this.current.classList.add(event.fail ? 'failed' : 'passed');
41
+ }
42
+ this.current = this.stack.pop();
43
+ break;
44
+ case 'comment':
45
+ {
46
+ const comment = document.createElement('div');
47
+ comment.className = 'comment text-comment';
48
+ comment.appendChild(document.createTextNode(event.name));
49
+ (this.current || this.root).appendChild(comment);
50
+ }
51
+ break;
52
+ case 'bail-out':
53
+ text = 'Bail out!';
54
+ event.name && (text += ' ' + event.name);
55
+ this.write(text, 'bail-out');
56
+ {
57
+ const bailOut = document.createElement('div');
58
+ bailOut.className = 'bail-out';
59
+ bailOut.appendChild(document.createTextNode(event.name));
60
+ this.root.appendChild(bailOut);
61
+ }
62
+ break;
63
+ case 'assert':
64
+ {
65
+ const assert = document.createElement('div');
66
+ assert.className = 'assert ' + (event.fail ? 'failed' : 'passed');
67
+ const header = document.createElement('div');
68
+ header.className = 'header';
69
+ if (event.skip) {
70
+ const node = document.createElement('span');
71
+ node.className = 'text-skipped';
72
+ node.appendChild(document.createTextNode('SKIP'));
73
+ header.appendChild(node);
74
+ header.appendChild(document.createTextNode(' '));
75
+ }
76
+ if (event.todo) {
77
+ const node = document.createElement('span');
78
+ node.className = 'text-todo';
79
+ node.appendChild(document.createTextNode('TODO'));
80
+ header.appendChild(node);
81
+ header.appendChild(document.createTextNode(' '));
82
+ }
83
+ header.appendChild(document.createTextNode(event.name));
84
+ assert.appendChild(header);
85
+ if (event.operator) {
86
+ const row = document.createElement('div'),
87
+ name = document.createElement('span'),
88
+ value = document.createElement('code');
89
+ row.className = 'data';
90
+ name.className = 'name';
91
+ name.appendChild(document.createTextNode('operator:'));
92
+ value.className = 'value';
93
+ value.appendChild(document.createTextNode(event.operator));
94
+ row.appendChild(name);
95
+ row.appendChild(value);
96
+ assert.appendChild(row);
97
+ }
98
+ if (event.hasOwnProperty('expected')) {
99
+ const row = document.createElement('div'),
100
+ name = document.createElement('span'),
101
+ value = document.createElement('code');
102
+ row.className = 'data';
103
+ name.className = 'name';
104
+ name.appendChild(document.createTextNode('expected:'));
105
+ value.className = 'value';
106
+ value.appendChild(document.createTextNode(formatValue(event.expected)));
107
+ row.appendChild(name);
108
+ row.appendChild(value);
109
+ assert.appendChild(row);
110
+ }
111
+ if (event.hasOwnProperty('actual')) {
112
+ const row = document.createElement('div'),
113
+ name = document.createElement('span'),
114
+ value = document.createElement('code');
115
+ row.className = 'data';
116
+ name.className = 'name';
117
+ name.appendChild(document.createTextNode('actual:'));
118
+ value.className = 'value';
119
+ value.appendChild(document.createTextNode(formatValue(event.actual)));
120
+ row.appendChild(name);
121
+ row.appendChild(value);
122
+ assert.appendChild(row);
123
+ }
124
+ if (event.at) {
125
+ const row = document.createElement('div'),
126
+ name = document.createElement('span'),
127
+ value = document.createElement('code');
128
+ row.className = 'data';
129
+ name.className = 'name';
130
+ name.appendChild(document.createTextNode('at:'));
131
+ value.className = 'value';
132
+ value.appendChild(document.createTextNode(event.at));
133
+ row.appendChild(name);
134
+ row.appendChild(value);
135
+ assert.appendChild(row);
136
+ }
137
+ {
138
+ const stack = event.actual && event.actual.type === 'Error' && typeof event.actual.stack == 'string' ? event.actual.stack : event.marker.stack;
139
+ if (typeof stack == 'string') {
140
+ const row = document.createElement('div'),
141
+ name = document.createElement('span'),
142
+ value = document.createElement('pre');
143
+ row.className = 'data stack';
144
+ name.className = 'name';
145
+ name.appendChild(document.createTextNode('stack:'));
146
+ value.className = 'value';
147
+ stack.split('\n').forEach(line => value.appendChild(document.createTextNode(line + '\n')));
148
+ row.appendChild(name);
149
+ row.appendChild(value);
150
+ assert.appendChild(row);
151
+ }
152
+ }
153
+ (this.current || this.root).appendChild(assert);
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ }
159
+
160
+ export default DomReporter;
@@ -0,0 +1,54 @@
1
+ import EventServer from '../src/utils/EventServer.js';
2
+
3
+ export default class TestWorker extends EventServer {
4
+ constructor(...args) {
5
+ super(...args);
6
+ this.counter = 0;
7
+ window.__tape6_reporter = (id, event) => {
8
+ this.report(id, event);
9
+ if (event.type === 'end' && event.test === 0) this.close(id);
10
+ };
11
+ window.__tape6_error = (id, error) => {
12
+ error && this.report(id, {type: 'comment', name: 'fail to load: ' + error.message, test: 0});
13
+ this.close(id);
14
+ };
15
+ }
16
+ makeTask(fileName) {
17
+ const id = String(++this.counter);
18
+ const iframe = document.createElement('iframe');
19
+ iframe.id = 'test-iframe-' + id;
20
+ iframe.className = 'test-iframe';
21
+ if (/\.html?$/i.test(fileName)) {
22
+ iframe.src = '/' + fileName + '?id=' + id + (this.options.failOnce ? '&flags=F' : '');
23
+ iframe.onerror = error => window.__tape6_error(id, error);
24
+ document.body.append(iframe);
25
+ } else {
26
+ document.body.append(iframe);
27
+ iframe.contentWindow.document.open();
28
+ iframe.contentWindow.document.write(`
29
+ <!doctype html>
30
+ <html>
31
+ <head>
32
+ <title>Test IFRAME</title>
33
+ <script type="module">
34
+ window.__tape6_id = ${JSON.stringify(id)};
35
+ window.__tape6_flags = "${this.options.failOnce ? 'F' : ''}";
36
+ const s = document.createElement('script');
37
+ s.setAttribute('type', 'module');
38
+ s.src = '/${fileName}';
39
+ s.onerror = error => window.parent.__tape6_error(window.__tape6_id, error);
40
+ document.documentElement.appendChild(s);
41
+ </script>
42
+ </head>
43
+ <body></body>
44
+ </html>
45
+ `);
46
+ iframe.contentWindow.document.close();
47
+ }
48
+ return id;
49
+ }
50
+ destroyTask(id) {
51
+ const iframe = document.getElementById('test-iframe-' + id);
52
+ iframe && iframe.parentElement.removeChild(iframe);
53
+ }
54
+ }
@@ -0,0 +1,235 @@
1
+ const TWO_PI = 2 * Math.PI;
2
+
3
+ const tmpl = (template, dict) => template.replace(/\$\{([^\}]*)\}/g, (_, name) => dict[name]);
4
+
5
+ export const makeSegment = (args, options) => {
6
+ // args is {startAngle, angle, index, className}
7
+ // optional index points to a data point
8
+ // optional className is a CSS class
9
+ // default startAngle=0 (in radians)
10
+ // default angle=2*PI (in radians)
11
+
12
+ // options is {center, innerRadius, radius, gap, precision, document}
13
+ // default center is {x=0, y=0}
14
+ // default innerRadius=0
15
+ // default radius=100
16
+ // default gap=0 (gap between segments in pixels)
17
+ // default precision=6 (digits after decimal point)
18
+ // default document is document
19
+
20
+ const node = (options.document || document).createElementNS('http://www.w3.org/2000/svg', 'path'),
21
+ center = options.center || {x: 0, y: 0},
22
+ innerRadius = Math.max(options.innerRadius || 0, 0),
23
+ radius = Math.max(options.radius || 100, innerRadius),
24
+ gap = Math.max(options.gap || 0, 0),
25
+ precision = options.precision || 6,
26
+ angle = typeof args.angle != 'number' || args.angle >= TWO_PI ? TWO_PI : args.angle,
27
+ startAngle = args.startAngle || 0;
28
+
29
+ let path;
30
+
31
+ const innerGapAngle = gap / innerRadius / 2,
32
+ gapAngle = gap / radius / 2,
33
+ cx = center.x,
34
+ cy = center.y,
35
+ data = {
36
+ cx: cx.toFixed(precision),
37
+ cy: cy.toFixed(precision),
38
+ r: radius.toFixed(precision)
39
+ };
40
+
41
+ if (angle >= TWO_PI) {
42
+ data.tr = (2 * radius).toFixed(precision);
43
+ // generate a circle
44
+ if (innerRadius <= 0) {
45
+ // a circle
46
+ path = tmpl('M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0z', data);
47
+ } else {
48
+ data.r0 = innerRadius.toFixed(precision);
49
+ data.tr0 = (2 * innerRadius).toFixed(precision);
50
+ // a donut
51
+ path = tmpl(
52
+ 'M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0zM${cx} ${cy}m -${r0} 0a${r0} ${r0} 0 1 1 ${tr0} 0a${r0} ${r0} 0 1 1 -${tr0} 0z',
53
+ data
54
+ );
55
+ }
56
+ } else {
57
+ const endAngle = startAngle + angle;
58
+ let start = startAngle + gapAngle,
59
+ finish = endAngle - gapAngle;
60
+ if (finish < start) {
61
+ start = finish = startAngle + angle / 2;
62
+ }
63
+ data.lg = angle > Math.PI ? 1 : 0;
64
+ data.x1 = (radius * Math.cos(start) + cx).toFixed(precision);
65
+ data.y1 = (radius * Math.sin(start) + cy).toFixed(precision);
66
+ data.x2 = (radius * Math.cos(finish) + cx).toFixed(precision);
67
+ data.y2 = (radius * Math.sin(finish) + cy).toFixed(precision);
68
+ if (innerRadius <= 0) {
69
+ // a pie slice
70
+ path = tmpl('M${cx} ${cy}L${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${cx} ${cy}z', data);
71
+ } else {
72
+ start = startAngle + innerGapAngle;
73
+ finish = endAngle - innerGapAngle;
74
+ if (finish < start) {
75
+ start = finish = startAngle + angle / 2;
76
+ }
77
+ data.r0 = innerRadius.toFixed(precision);
78
+ data.x3 = (innerRadius * Math.cos(finish) + cx).toFixed(precision);
79
+ data.y3 = (innerRadius * Math.sin(finish) + cy).toFixed(precision);
80
+ data.x4 = (innerRadius * Math.cos(start) + cx).toFixed(precision);
81
+ data.y4 = (innerRadius * Math.sin(start) + cy).toFixed(precision);
82
+ // a segment
83
+ path = tmpl('M${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${x3} ${y3}A${r0} ${r0} 0 ${lg} 0 ${x4} ${y4}L${x1} ${y1}z', data);
84
+ }
85
+ }
86
+ node.setAttribute('d', path);
87
+ if ('index' in args) {
88
+ node.setAttribute('data-index', args.index);
89
+ }
90
+ if (args.className) {
91
+ node.setAttribute('class', args.className);
92
+ }
93
+ return node;
94
+ };
95
+
96
+ export const processPieRun = (data, options) => {
97
+ // data is [datum, datum...]
98
+ // datum is {value, className, skip, hide}
99
+ // value is a positive number
100
+ // className is an optional CSS class name
101
+ // skip is a flag (default: false) to skip this segment completely
102
+ // hide is a flag (default: false) to suppress rendering
103
+
104
+ // options is {center, innerRadius, radius, startAngle, minSizeInPx, skipIfLessInPx, emptyClass, precision}
105
+ // default center is {x=0, y=0}
106
+ // default innerRadius=0
107
+ // default radius=100
108
+ // default startAngle=0 (in radians)
109
+ // default gap=0 (gap between segments in pixels)
110
+ // default precision=6 (digits after decimal point)
111
+ // minSizeInPx is to make non-empty segments at least this big (default: 0).
112
+ // skipIfLessInPx is a threshold (default: 0), when to skip too small segments.
113
+ // emptyClass is a CSS class name for an empty run
114
+
115
+ const radius = Math.max(options.radius || 100, options.innerRadius || 0, 0),
116
+ gap = Math.max(options.gap || 0, 0),
117
+ minSizeInPx = Math.max(options.minSizeInPx || 0, 0),
118
+ skipIfLessInPx = Math.max(options.skipIfLessInPx || 0, gap),
119
+ runOptions = {
120
+ center: options.center,
121
+ innerRadius: options.innerRadius,
122
+ radius: radius,
123
+ gap: gap,
124
+ precision: options.precision,
125
+ document: options.document
126
+ };
127
+
128
+ // sanitize data
129
+ data.forEach((datum, index) => {
130
+ if (!datum.skip && (isNaN(datum.value) || datum.value === null || datum.value <= 0)) {
131
+ datum.skip = true;
132
+ }
133
+ datum.index = index;
134
+ });
135
+
136
+ const total = data.reduce((acc, datum) => (datum.skip ? acc : acc + datum.value), 0);
137
+
138
+ let node;
139
+ if (total <= 0) {
140
+ // empty run
141
+ node = makeSegment(
142
+ {
143
+ index: -1, // to denote that it is not an actionable node
144
+ className: options.emptyClass
145
+ },
146
+ runOptions
147
+ );
148
+ return [node];
149
+ }
150
+
151
+ const nonEmptyDatumNumber = data.reduce((acc, datum) => (datum.skip ? acc : acc + 1), 0);
152
+
153
+ if (nonEmptyDatumNumber === 1) {
154
+ data.some(datum => {
155
+ if (datum.skip) return false;
156
+ node = makeSegment(
157
+ {
158
+ index: datum.index,
159
+ className: datum.className
160
+ },
161
+ runOptions
162
+ );
163
+ return true;
164
+ });
165
+ return [node];
166
+ }
167
+
168
+ // find too small segments
169
+ const sizes = data.map(datum => {
170
+ let angle = 0;
171
+ if (!datum.skip) {
172
+ angle = (datum.value / total) * TWO_PI;
173
+ }
174
+ return {angle: angle, index: datum.index};
175
+ });
176
+
177
+ let minAngle, newTotal, changeRatio;
178
+ if (minSizeInPx > 0) {
179
+ // adjust angles
180
+ minAngle = (minSizeInPx + gap) / radius;
181
+ sizes.forEach((size, index) => {
182
+ const datum = data[index];
183
+ if (!datum.skip && !datum.hide && size.angle < minAngle) {
184
+ size.angle = minAngle;
185
+ }
186
+ });
187
+ newTotal = sizes.reduce((acc, size) => acc + size.angle, 0);
188
+ const excess = newTotal - total,
189
+ totalForLargeAngles = sizes.reduce((acc, size) => (size.angle <= minAngle ? acc : acc + size.angle), 0);
190
+ changeRatio = (totalForLargeAngles - excess) / totalForLargeAngles;
191
+ sizes.forEach(size => {
192
+ if (size.angle > minAngle) {
193
+ size.angle *= changeRatio;
194
+ }
195
+ });
196
+ } else if (skipIfLessInPx > 0) {
197
+ // suppress angles
198
+ minAngle = skipIfLessInPx / radius;
199
+ sizes.forEach((size, index) => {
200
+ const datum = data[index];
201
+ if (!datum.skip && !datum.hide && size.angle < minAngle) {
202
+ size.angle = 0;
203
+ }
204
+ });
205
+ newTotal = sizes.reduce((acc, size) => acc + size.angle, 0);
206
+ changeRatio = TWO_PI / newTotal;
207
+ sizes.forEach(size => {
208
+ if (size.angle > 0) {
209
+ size.angle *= changeRatio;
210
+ }
211
+ });
212
+ }
213
+
214
+ // generate shape objects
215
+ const shapes = [];
216
+ let startAngle = options.startAngle || 0;
217
+ data.forEach((datum, index) => {
218
+ if (!datum.skip) {
219
+ const angle = sizes[index].angle;
220
+ if (!datum.hide) {
221
+ shapes.push({
222
+ index: index,
223
+ startAngle: startAngle,
224
+ angle: angle,
225
+ className: datum.className
226
+ });
227
+ }
228
+ startAngle += angle;
229
+ }
230
+ });
231
+
232
+ return shapes.map(shape => makeSegment(shape, runOptions));
233
+ };
234
+
235
+ export const addShapes = parent => node => parent.appendChild(node);