modest_canvas_rails 0.1.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.
@@ -0,0 +1,5 @@
1
+ //= require ./d3
2
+ //= require ./donut_chart
3
+ //= require ./edge_bundling
4
+ //= require ./scatter_plot
5
+ //= require ./word_cloud
@@ -0,0 +1,117 @@
1
+ var ModestCanvas = typeof ModestCanvas != "undefined" ? ModestCanvas : {};
2
+ ModestCanvas.scatterPlot = function(container, scatterData, args){
3
+ var elementContainer = d3.select(container);
4
+ var margin = {top: 20, right: 20, bottom: 30, left: 40},
5
+ width = elementContainer.node().getBoundingClientRect().width - margin.left - margin.right,
6
+ height = 400 - margin.top - margin.bottom;
7
+
8
+ var svg = elementContainer
9
+ .classed('d3_scatterplot', true)
10
+ .append("svg")
11
+ .attr("preserveAspectRatio", "xMinYMin meet")
12
+ .attr("viewBox", "0 0 " + (width + margin.left + margin.right) + " " + (height + margin.top + margin.bottom))
13
+ .append("g")
14
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
15
+
16
+ var minCircleFillColor = args.minCircleFillColor || "#000000";
17
+ var maxCircleFillColor = args.maxCircleFillColor || "#BBBBBB";
18
+
19
+ var color = d3.scaleLinear().domain(scatterData.colors.domain).range([minCircleFillColor, maxCircleFillColor]);
20
+
21
+ var xValue = function(d) { return d.coordinates.x;},
22
+ xScale = d3.scaleLinear().range([0, width]),
23
+ xMap = function(d) { return xScale(xValue(d));},
24
+ xAxis = d3.axisBottom(xScale);
25
+
26
+ var yValue = function(d) { return d.coordinates.y;},
27
+ yScale = d3.scaleLinear().range([height, 0]),
28
+ yMap = function(d) { return yScale(yValue(d));},
29
+ yAxis = d3.axisLeft(yScale);
30
+
31
+ if(scatterData.axes.enabled == true){
32
+ xScale.domain([d3.min(scatterData.values, xValue) - 0.2, d3.max(scatterData.values, xValue) + 0.2]);
33
+ yScale.domain([d3.min(scatterData.values, yValue) - 1, d3.max(scatterData.values, yValue) + 1]);
34
+
35
+ if(scatterData.axes.x.customTicks != undefined){
36
+ xAxis.tickFormat(function(d){return (scatterData.axes.x.customTicks[d] != undefined) ? scatterData.axes.x.customTicks[d] : "";});
37
+ }
38
+
39
+ if(scatterData.axes.y.customTicks != undefined){
40
+ yAxis.tickFormat(function(d){return (scatterData.axes.y.customTicks[d] != undefined) ? scatterData.axes.y.customTicks[d] : "";});
41
+ }
42
+
43
+ svg.append("g")
44
+ .attr("class", "x axis")
45
+ .attr("transform", "translate(0," + height + ")")
46
+ .call(xAxis)
47
+ .append("text")
48
+ .attr("class", "label")
49
+ .attr("x", width)
50
+ .attr("y", -6)
51
+ .style("text-anchor", "end")
52
+ .text(scatterData.axes.x.label);
53
+
54
+ svg.append("g")
55
+ .attr("class", "y axis")
56
+ .call(yAxis)
57
+ .append("text")
58
+ .attr("class", "label")
59
+ .attr("transform", "rotate(-90)")
60
+ .attr("y", 6)
61
+ .attr("dy", ".71em")
62
+ .style("text-anchor", "end")
63
+ .text(scatterData.axes.y.label);
64
+ }
65
+
66
+ var circles = svg.selectAll(".dot")
67
+ .data(scatterData.values)
68
+ .enter().append("circle")
69
+ .attr("class", "dot")
70
+ .attr("r", 5.5)
71
+ .attr("cx", xMap)
72
+ .attr("cy", yMap)
73
+ .attr("onclick", function(d) {return (d.point.attributes.onclick != undefined) ? d.point.attributes.onclick : "";})
74
+ .style("fill", function(d) { return (scatterData.colors.x_based == true) ? color(d.coordinates.x) : color(d.coordinates.y);});
75
+
76
+ if(scatterData.tooltip.enabled == true){
77
+ var tooltip = elementContainer.append("div")
78
+ .attr("class", "tooltip")
79
+ .style("opacity", 0);
80
+ circles.on("mouseover", function(d) {
81
+ tooltip.transition()
82
+ .duration(200)
83
+ .style("opacity", .9);
84
+ tooltip.html(d.tooltip.text)
85
+ .style("left", (d3.event.pageX - elementContainer.node().getBoundingClientRect().left - margin.left + 45) + "px")
86
+ .style("top", (d3.event.pageY - elementContainer.node().getBoundingClientRect().top + margin.top - 28) + "px");
87
+ });
88
+ circles.on("mouseout", function(d) {
89
+ tooltip.transition()
90
+ .duration(500)
91
+ .style("opacity", 0);
92
+ });
93
+ }
94
+
95
+ if(scatterData.legend.enabled == true){
96
+ var legend = svg.selectAll(".legend")
97
+ .data(scatterData.legend.domain)
98
+ .enter().append("g")
99
+ .attr("class", "legend")
100
+ .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
101
+
102
+ legend.append("rect")
103
+ .attr("x", width - 18)
104
+ .attr("rx", 18)
105
+ .attr("ry", 18)
106
+ .attr("width", 18)
107
+ .attr("height", 18)
108
+ .style("fill", function(d){return color(d.for);});
109
+
110
+ legend.append("text")
111
+ .attr("x", width - 24)
112
+ .attr("y", 9)
113
+ .attr("dy", ".35em")
114
+ .style("text-anchor", "end")
115
+ .text(function(d) { return d.label;})
116
+ }
117
+ }
@@ -0,0 +1,478 @@
1
+ // Word cloud layout by Jason Davies, http://www.jasondavies.com/word-cloud/
2
+ // Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
3
+ var ModestCanvas = typeof ModestCanvas != "undefined" ? ModestCanvas : {};
4
+ ModestCanvas.wordCloud = function(container, cloudData, args){
5
+ var jsonData = cloudData;
6
+ d3WordCloudInit();
7
+ if(args == undefined){args = {}}
8
+ var maxFontSize = args.maxFontSize || 50;
9
+ var minFontSize = args.minFontSize || 14;
10
+ var minCategoryFillColor = args.minCategoryFillColor || "#7c68a9";
11
+ var maxCategoryFillColor = args.maxCategoryFillColor || "#80696a";
12
+
13
+ var elementContainer = d3.select(container);
14
+ var margin = {top: 20, right: 20, bottom: 40, left: 20},
15
+ width = elementContainer.node().getBoundingClientRect().width - margin.left - margin.right,
16
+ height = 400 - margin.top - margin.bottom;
17
+ var categories = d3.nest().key(function(d) { return d.category; }).map(jsonData).keys();
18
+ var maxFrequency = Math.max.apply(Math, d3.nest().key(function(d) { return d.frequency; }).map(jsonData).keys());
19
+ var minFrequency = Math.min.apply(Math, d3.nest().key(function(d) { return d.frequency; }).map(jsonData).keys());
20
+ var fontSize = d3.scalePow().exponent(2).domain([minFrequency, maxFrequency]).range([minFontSize,maxFontSize]);
21
+ var color = d3.scaleOrdinal().domain(categories).range(d3.range(categories.length).map(d3.scaleLinear().domain([0, categories.length - 1]).range([minCategoryFillColor, maxCategoryFillColor]).interpolate(d3.interpolateLab)));
22
+ var layout = d3.cloud()
23
+ .timeInterval(10)
24
+ .size([width, height])
25
+ .words(jsonData)
26
+ .rotate(function(d) { return Math.random() > 0.5 ? 0 : -90; })
27
+ .fontSize(function(d,i) { return fontSize(d.frequency); })
28
+ .text(function(d) { return d.word; })
29
+ .spiral("archimedean")
30
+ .on("end", draw)
31
+ .start();
32
+
33
+ var svg = elementContainer
34
+ .classed('d3_word_cloud', true)
35
+ .append("svg")
36
+ .classed('d3_word_cloud_svg', true)
37
+ .attr("preserveAspectRatio", "xMinYMin meet")
38
+ .attr("viewBox", "0 0 " + (width + margin.left + margin.right) + " " + (height + margin.top + margin.bottom))
39
+ .append("g")
40
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
41
+
42
+ var wordcloud = svg.append("g")
43
+ .attr('class','wordcloud')
44
+ .attr("transform", "translate(" + width/2 + "," + height/2 + ")");
45
+
46
+ if(categories.length && args.hideCategoryAxis != true){
47
+ var x0 = d3.scaleBand()
48
+ .range([0, width])
49
+ .round(.1)
50
+ .domain(categories);
51
+
52
+ var xAxis = d3.axisBottom()
53
+ .scale(x0);
54
+
55
+ svg.append("g")
56
+ .attr("class", "x axis")
57
+ .classed('categories_axis', true)
58
+ .attr("transform", "translate(0," + height + ")")
59
+ .call(xAxis)
60
+ .selectAll('text')
61
+ .style('fill',function(d) { return color(d); });
62
+ }
63
+
64
+ function draw(words) {
65
+ wordcloud.selectAll("text")
66
+ .data(words)
67
+ .enter().append("text")
68
+ .attr('class','word')
69
+ .style("font-size", function(d) { return d.size + "px"; })
70
+ .style("fill", function(d) {
71
+ return color(d.category);
72
+ })
73
+ .attr("text-anchor", "middle")
74
+ .attr("transform", function(d) { return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; })
75
+ .text(function(d) { return d.text; });
76
+ };
77
+
78
+ function d3WordCloudInit() {
79
+ if (typeof define === "function" && define.amd) define(["d3"], cloud);
80
+ else cloud(this.d3);
81
+ var cloudRadians = Math.PI / 180,
82
+ cw = 1 << 11 >> 5,
83
+ ch = 1 << 11;
84
+
85
+ function cloud(d3) {
86
+ d3.cloud = function cloud() {
87
+ var size = [256, 256],
88
+ text = cloudText,
89
+ font = cloudFont,
90
+ fontSize = cloudFontSize,
91
+ fontStyle = cloudFontNormal,
92
+ fontWeight = cloudFontNormal,
93
+ rotate = cloudRotate,
94
+ padding = cloudPadding,
95
+ spiral = archimedeanSpiral,
96
+ words = [],
97
+ timeInterval = Infinity,
98
+ event = d3.dispatch("word", "end"),
99
+ timer = null,
100
+ random = Math.random,
101
+ cloud = {},
102
+ canvas = cloudCanvas;
103
+
104
+ cloud.canvas = function(_) {
105
+ return arguments.length ? (canvas = functor(_), cloud) : canvas;
106
+ };
107
+
108
+ cloud.start = function() {
109
+ var contextAndRatio = getContext(canvas()),
110
+ board = zeroArray((size[0] >> 5) * size[1]),
111
+ bounds = null,
112
+ n = words.length,
113
+ i = -1,
114
+ tags = [],
115
+ data = words.map(function(d, i) {
116
+ d.text = text.call(this, d, i);
117
+ d.font = font.call(this, d, i);
118
+ d.style = fontStyle.call(this, d, i);
119
+ d.weight = fontWeight.call(this, d, i);
120
+ d.rotate = rotate.call(this, d, i);
121
+ d.size = ~~fontSize.call(this, d, i);
122
+ d.padding = padding.call(this, d, i);
123
+ return d;
124
+ }).sort(function(a, b) { return b.size - a.size; });
125
+
126
+ if (timer) clearInterval(timer);
127
+ timer = setInterval(step, 0);
128
+ step();
129
+
130
+ return cloud;
131
+
132
+ function step() {
133
+ var start = Date.now();
134
+ while (Date.now() - start < timeInterval && ++i < n && timer) {
135
+ var d = data[i];
136
+ d.x = (size[0] * (random() + .5)) >> 1;
137
+ d.y = (size[1] * (random() + .5)) >> 1;
138
+ cloudSprite(contextAndRatio, d, data, i);
139
+ if (d.hasText && place(board, d, bounds)) {
140
+ tags.push(d);
141
+ event.call("word", cloud, d);
142
+ if (bounds) cloudBounds(bounds, d);
143
+ else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
144
+ // Temporary hack
145
+ d.x -= size[0] >> 1;
146
+ d.y -= size[1] >> 1;
147
+ }
148
+ }
149
+ if (i >= n) {
150
+ cloud.stop();
151
+ event.call("end", cloud, tags, bounds);
152
+ }
153
+ }
154
+ }
155
+
156
+ cloud.stop = function() {
157
+ if (timer) {
158
+ clearInterval(timer);
159
+ timer = null;
160
+ }
161
+ return cloud;
162
+ };
163
+
164
+ function getContext(canvas) {
165
+ canvas.width = canvas.height = 1;
166
+ var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
167
+ canvas.width = (cw << 5) / ratio;
168
+ canvas.height = ch / ratio;
169
+
170
+ var context = canvas.getContext("2d");
171
+ context.fillStyle = context.strokeStyle = "red";
172
+ context.textAlign = "center";
173
+
174
+ return {context: context, ratio: ratio};
175
+ }
176
+
177
+ function place(board, tag, bounds) {
178
+ var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
179
+ startX = tag.x,
180
+ startY = tag.y,
181
+ maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
182
+ s = spiral(size),
183
+ dt = random() < .5 ? 1 : -1,
184
+ t = -dt,
185
+ dxdy,
186
+ dx,
187
+ dy;
188
+
189
+ while (dxdy = s(t += dt)) {
190
+ dx = ~~dxdy[0];
191
+ dy = ~~dxdy[1];
192
+
193
+ if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
194
+
195
+ tag.x = startX + dx;
196
+ tag.y = startY + dy;
197
+
198
+ if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
199
+ tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
200
+ // TODO only check for collisions within current bounds.
201
+ if (!bounds || !cloudCollide(tag, board, size[0])) {
202
+ if (!bounds || collideRects(tag, bounds)) {
203
+ var sprite = tag.sprite,
204
+ w = tag.width >> 5,
205
+ sw = size[0] >> 5,
206
+ lx = tag.x - (w << 4),
207
+ sx = lx & 0x7f,
208
+ msx = 32 - sx,
209
+ h = tag.y1 - tag.y0,
210
+ x = (tag.y + tag.y0) * sw + (lx >> 5),
211
+ last;
212
+ for (var j = 0; j < h; j++) {
213
+ last = 0;
214
+ for (var i = 0; i <= w; i++) {
215
+ board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
216
+ }
217
+ x += sw;
218
+ }
219
+ delete tag.sprite;
220
+ return true;
221
+ }
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+
227
+ cloud.timeInterval = function(_) {
228
+ return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
229
+ };
230
+
231
+ cloud.words = function(_) {
232
+ return arguments.length ? (words = _, cloud) : words;
233
+ };
234
+
235
+ cloud.size = function(_) {
236
+ return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
237
+ };
238
+
239
+ cloud.font = function(_) {
240
+ return arguments.length ? (font = functor(_), cloud) : font;
241
+ };
242
+
243
+ cloud.fontStyle = function(_) {
244
+ return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
245
+ };
246
+
247
+ cloud.fontWeight = function(_) {
248
+ return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
249
+ };
250
+
251
+ cloud.rotate = function(_) {
252
+ return arguments.length ? (rotate = functor(_), cloud) : rotate;
253
+ };
254
+
255
+ cloud.text = function(_) {
256
+ return arguments.length ? (text = functor(_), cloud) : text;
257
+ };
258
+
259
+ cloud.spiral = function(_) {
260
+ return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
261
+ };
262
+
263
+ cloud.fontSize = function(_) {
264
+ return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
265
+ };
266
+
267
+ cloud.padding = function(_) {
268
+ return arguments.length ? (padding = functor(_), cloud) : padding;
269
+ };
270
+
271
+ cloud.random = function(_) {
272
+ return arguments.length ? (random = _, cloud) : random;
273
+ };
274
+
275
+ cloud.on = function() {
276
+ var value = event.on.apply(event, arguments);
277
+ return value === event ? cloud : value;
278
+ };
279
+
280
+ return cloud;
281
+ };
282
+
283
+ function cloudText(d) {
284
+ return d.text;
285
+ }
286
+
287
+ function cloudFont() {
288
+ return "serif";
289
+ }
290
+
291
+ function cloudFontNormal() {
292
+ return "normal";
293
+ }
294
+
295
+ function cloudFontSize(d) {
296
+ return Math.sqrt(d.value);
297
+ }
298
+
299
+ function cloudRotate() {
300
+ return (~~(Math.random() * 6) - 3) * 30;
301
+ }
302
+
303
+ function cloudPadding() {
304
+ return 1;
305
+ }
306
+
307
+ // Fetches a monochrome sprite bitmap for the specified text.
308
+ // Load in batches for speed.
309
+ function cloudSprite(contextAndRatio, d, data, di) {
310
+ if (d.sprite) return;
311
+ var c = contextAndRatio.context,
312
+ ratio = contextAndRatio.ratio;
313
+
314
+ c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
315
+ var x = 0,
316
+ y = 0,
317
+ maxh = 0,
318
+ n = data.length;
319
+ --di;
320
+ while (++di < n) {
321
+ d = data[di];
322
+ c.save();
323
+ c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
324
+ var w = c.measureText(d.text + "m").width * ratio,
325
+ h = d.size << 1;
326
+ if (d.rotate) {
327
+ var sr = Math.sin(d.rotate * cloudRadians),
328
+ cr = Math.cos(d.rotate * cloudRadians),
329
+ wcr = w * cr,
330
+ wsr = w * sr,
331
+ hcr = h * cr,
332
+ hsr = h * sr;
333
+ w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
334
+ h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
335
+ } else {
336
+ w = (w + 0x1f) >> 5 << 5;
337
+ }
338
+ if (h > maxh) maxh = h;
339
+ if (x + w >= (cw << 5)) {
340
+ x = 0;
341
+ y += maxh;
342
+ maxh = 0;
343
+ }
344
+ if (y + h >= ch) break;
345
+ c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
346
+ if (d.rotate) c.rotate(d.rotate * cloudRadians);
347
+ c.fillText(d.text, 0, 0);
348
+ if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
349
+ c.restore();
350
+ d.width = w;
351
+ d.height = h;
352
+ d.xoff = x;
353
+ d.yoff = y;
354
+ d.x1 = w >> 1;
355
+ d.y1 = h >> 1;
356
+ d.x0 = -d.x1;
357
+ d.y0 = -d.y1;
358
+ d.hasText = true;
359
+ x += w;
360
+ }
361
+ var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
362
+ sprite = [];
363
+ while (--di >= 0) {
364
+ d = data[di];
365
+ if (!d.hasText) continue;
366
+ var w = d.width,
367
+ w32 = w >> 5,
368
+ h = d.y1 - d.y0;
369
+ // Zero the buffer
370
+ for (var i = 0; i < h * w32; i++) sprite[i] = 0;
371
+ x = d.xoff;
372
+ if (x == null) return;
373
+ y = d.yoff;
374
+ var seen = 0,
375
+ seenRow = -1;
376
+ for (var j = 0; j < h; j++) {
377
+ for (var i = 0; i < w; i++) {
378
+ var k = w32 * j + (i >> 5),
379
+ m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
380
+ sprite[k] |= m;
381
+ seen |= m;
382
+ }
383
+ if (seen) seenRow = j;
384
+ else {
385
+ d.y0++;
386
+ h--;
387
+ j--;
388
+ y++;
389
+ }
390
+ }
391
+ d.y1 = d.y0 + seenRow;
392
+ d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
393
+ }
394
+ }
395
+
396
+ // Use mask-based collision detection.
397
+ function cloudCollide(tag, board, sw) {
398
+ sw >>= 5;
399
+ var sprite = tag.sprite,
400
+ w = tag.width >> 5,
401
+ lx = tag.x - (w << 4),
402
+ sx = lx & 0x7f,
403
+ msx = 32 - sx,
404
+ h = tag.y1 - tag.y0,
405
+ x = (tag.y + tag.y0) * sw + (lx >> 5),
406
+ last;
407
+ for (var j = 0; j < h; j++) {
408
+ last = 0;
409
+ for (var i = 0; i <= w; i++) {
410
+ if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
411
+ & board[x + i]) return true;
412
+ }
413
+ x += sw;
414
+ }
415
+ return false;
416
+ }
417
+
418
+ function cloudBounds(bounds, d) {
419
+ var b0 = bounds[0],
420
+ b1 = bounds[1];
421
+ if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
422
+ if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
423
+ if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
424
+ if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
425
+ }
426
+
427
+ function collideRects(a, b) {
428
+ return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
429
+ }
430
+
431
+ function archimedeanSpiral(size) {
432
+ var e = size[0] / size[1];
433
+ return function(t) {
434
+ return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
435
+ };
436
+ }
437
+
438
+ function rectangularSpiral(size) {
439
+ var dy = 4,
440
+ dx = dy * size[0] / size[1],
441
+ x = 0,
442
+ y = 0;
443
+ return function(t) {
444
+ var sign = t < 0 ? -1 : 1;
445
+ // See triangular numbers: T_n = n * (n + 1) / 2.
446
+ switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
447
+ case 0: x += dx; break;
448
+ case 1: y += dy; break;
449
+ case 2: x -= dx; break;
450
+ default: y -= dy; break;
451
+ }
452
+ return [x, y];
453
+ };
454
+ }
455
+
456
+ // TODO reuse arrays?
457
+ function zeroArray(n) {
458
+ var a = [],
459
+ i = -1;
460
+ while (++i < n) a[i] = 0;
461
+ return a;
462
+ }
463
+
464
+ function cloudCanvas() {
465
+ return document.createElement("canvas");
466
+ }
467
+
468
+ function functor(d) {
469
+ return typeof d === "function" ? d : function() { return d; };
470
+ }
471
+
472
+ var spirals = {
473
+ archimedean: archimedeanSpiral,
474
+ rectangular: rectangularSpiral
475
+ }
476
+ }
477
+ }
478
+ }