modest_canvas_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }