├── .gitignore
├── tagcloud.png
├── public
├── cloud.less
├── cloud.html
├── vis
│ ├── components
│ │ ├── utils
│ │ │ ├── valuator.js
│ │ │ ├── builder.js
│ │ │ └── attrs.js
│ │ ├── layout
│ │ │ ├── generator.js
│ │ │ └── layout.js
│ │ ├── elements
│ │ │ ├── g.js
│ │ │ └── text.js
│ │ ├── visualization
│ │ │ ├── generator.js
│ │ │ └── tag_cloud.js
│ │ ├── control
│ │ │ └── events.js
│ │ └── d3.layout.cloud
│ │ │ ├── d3-dispatch.js
│ │ │ └── d3.layout.cloud.js
│ └── index.js
├── lib
│ ├── cloud_controller.js
│ └── cloud_directive.js
├── cloud.js
└── cloud_vis_params.html
├── index.js
├── package.json
├── LICENSE
├── README.md
└── gulpfile.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/tagcloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stormpython/tagcloud/HEAD/tagcloud.png
--------------------------------------------------------------------------------
/public/cloud.less:
--------------------------------------------------------------------------------
1 | .tagcloud-vis {
2 | display: flex;
3 | flex: 1 1 100%;
4 | position: relative;
5 | }
6 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (kibana) {
2 | return new kibana.Plugin({
3 | uiExports: {
4 | visTypes: ['plugins/tagcloud/cloud']
5 | }
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/public/cloud.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/vis/components/utils/valuator.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 |
4 | function valuator(v) {
5 | if (_.isFunction(v)) { return v; }
6 | if (_.isString(v) || _.isNumber(v)) {
7 | return function (d) { return d[v]; };
8 | }
9 | return d3.functor(v);
10 | };
11 |
12 | module.exports = valuator;
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tagcloud",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "d3": "3.5.12",
6 | "d3.layout.cloud": "1.2.0",
7 | "lodash": "3.10.1"
8 | },
9 | "devDependencies": {
10 | "bluebird": "3.1.1",
11 | "gulp": "3.9.0",
12 | "gulp-batch": "1.0.5",
13 | "gulp-eslint": "1.1.1",
14 | "gulp-watch": "4.3.5",
15 | "mkdirp": "0.5.1",
16 | "path": "0.12.7",
17 | "rsync": "0.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/public/vis/components/utils/builder.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 |
4 | function builder(obj, func) {
5 | if (!_.isPlainObject(obj)) {
6 | throw new Error('builder expects a javascript Object ({}) as its first argument');
7 | }
8 |
9 | if (!_.isFunction(func)) {
10 | throw new Error('builder expects a function as its second argument');
11 | }
12 |
13 | d3.entries(obj).forEach(function (d) {
14 | if (_.isFunction(func[d.key])) {
15 | func[d.key](d.value);
16 | }
17 | });
18 |
19 | return func;
20 | };
21 |
22 | module.exports = builder;
23 |
--------------------------------------------------------------------------------
/public/lib/cloud_controller.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 |
3 | var module = require('ui/modules').get('tagcloud');
4 |
5 | module.controller('CloudController', function ($scope) {
6 | $scope.$watch('esResponse', function (resp) {
7 | if (!resp) {
8 | $scope.data = null;
9 | return;
10 | }
11 |
12 | var tagsAggId = _.first(_.pluck($scope.vis.aggs.bySchemaName['segment'], 'id'));
13 | var metricsAgg = _.first($scope.vis.aggs.bySchemaName['metric']);
14 |
15 | var buckets = resp.aggregations[tagsAggId].buckets;
16 |
17 | var tags = buckets.map(function (bucket) {
18 | return {
19 | text: bucket.key,
20 | size: metricsAgg.getValue(bucket)
21 | };
22 | });
23 |
24 | $scope.data = [{ tags: tags}];
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/public/vis/components/layout/generator.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var attrs = require('plugins/tagcloud/vis/components/utils/attrs');
3 | var baseLayout = require('plugins/tagcloud/vis/components/layout/layout');
4 | var gGenerator = require('plugins/tagcloud/vis/components/elements/g');
5 |
6 | function layoutGenerator() {
7 | var layout = baseLayout();
8 | var group = gGenerator();
9 |
10 | function generator(selection) {
11 | selection.each(function (data) {
12 | group.cssClass('chart')
13 | .transform(function (d) {
14 | return 'translate(' + d.dx + ',' + d.dy + ')';
15 | });
16 |
17 | d3.select(this)
18 | .datum(layout(data))
19 | .call(group);
20 | });
21 | }
22 |
23 | // Public API
24 | generator.attr = attrs(generator)(layout);
25 |
26 | return generator;
27 | }
28 |
29 | module.exports = layoutGenerator;
30 |
--------------------------------------------------------------------------------
/public/vis/components/elements/g.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 |
4 | function gGenerator() {
5 | var cssClass = 'group';
6 | var transform = 'translate(0,0)';
7 |
8 | function generator(selection) {
9 | selection.each(function (data, index) {
10 | var g = d3.select(this).selectAll('g.' + cssClass)
11 | .data(data);
12 |
13 | g.exit().remove();
14 |
15 | g.enter().append('g')
16 | .attr('class', cssClass);
17 |
18 | g.attr('transform', transform);
19 | });
20 | }
21 |
22 | // Public API
23 | generator.cssClass = function (v) {
24 | if (!arguments.length) { return cssClass; }
25 | cssClass = _.isString(v) ? v : cssClass;
26 | return generator;
27 | };
28 |
29 | generator.transform = function (v) {
30 | if (!arguments.length) { return transform; }
31 | transform = d3.functor(v);
32 | return generator;
33 | };
34 |
35 | return generator;
36 | }
37 |
38 | module.exports = gGenerator;
39 |
--------------------------------------------------------------------------------
/public/vis/components/visualization/generator.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 | var builder = require('plugins/tagcloud/vis/components/utils/builder');
4 | var tagCloud = require('plugins/tagcloud/vis/components/visualization/tag_cloud');
5 |
6 | function chartGenerator() {
7 | var opts = {};
8 |
9 | function generator(selection) {
10 | selection.each(function (data) {
11 | var dataOpts = (data && data.options) || {};
12 | var accessor = opts.accessor || dataOpts.accessor || 'tags';
13 | var chart = tagCloud()
14 | .width(data.width)
15 | .height(data.height)
16 | .accessor(accessor);
17 |
18 | _.forEach([opts, dataOpts], function (o) {
19 | builder(o, chart);
20 | });
21 |
22 | d3.select(this).call(chart); // Draw Chart
23 | });
24 | }
25 |
26 | // Public API
27 | generator.options = function (v) {
28 | if (!arguments.length) { return opts; }
29 | opts = _.isPlainObject(v) && !_.isArray(v) ? v : opts;
30 | return generator;
31 | };
32 |
33 | return generator;
34 | }
35 |
36 | module.exports = chartGenerator;
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Shelby Sturgis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/public/vis/components/control/events.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 |
4 | // Adds event listeners to DOM elements
5 | function events() {
6 | var processor = function (e) { return e; };
7 | var listeners = {};
8 | var svg;
9 |
10 | function control(selection) {
11 | selection.each(function () {
12 | svg = d3.select(this);
13 |
14 | d3.entries(listeners).forEach(function (d) {
15 | svg.on(d.key, function () {
16 | d3.event.stopPropagation(); // => event.stopPropagation()
17 |
18 | _.forEach(d.value, function (listener) {
19 | listener.call(this, processor(d3.event));
20 | });
21 | });
22 | });
23 | });
24 | }
25 |
26 | // Public API
27 | control.processor = function (v) {
28 | if (!arguments.length) { return processor; }
29 | processor = _.isFunction(v) ? v : processor;
30 | return control;
31 | };
32 |
33 | control.listeners = function (v) {
34 | if (!arguments.length) { return listeners; }
35 | listeners = _.isPlainObject(v) ? v : listeners;
36 | return control;
37 | };
38 |
39 | return control;
40 | };
41 |
42 | module.exports = events;
43 |
--------------------------------------------------------------------------------
/public/vis/components/utils/attrs.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var builder = require('plugins/tagcloud/vis/components/utils/builder');
3 |
4 | function attrs(generator) {
5 | return function () {
6 | var funcs = _.toArray(arguments);
7 |
8 | function filterFunctions(arr, attr) {
9 | return _.filter(arr, function (func) {
10 | return _.isFunction(func[attr]);
11 | });
12 | }
13 |
14 | function getValue(arr, attr) {
15 | if (!arr.length) { return; }
16 |
17 | if (arr.length === 1) { return arr[0][attr](); }
18 |
19 | return _.map(arr, function (func) {
20 | return func[attr]();
21 | });
22 | }
23 |
24 | return function (attr, value) {
25 | if (_.isString(attr)) {
26 | if (!value) {
27 | return getValue(filterFunctions(funcs, attr), attr);
28 | }
29 |
30 | _.forEach(filter(funcs, attr), function (func) {
31 | func[attr](value);
32 | });
33 | }
34 |
35 | if (!value && _.isPlainObject(attr)) {
36 | _.forEach(funcs, function (func) {
37 | builder(attr, func);
38 | });
39 | }
40 |
41 | return generator;
42 | };
43 | };
44 | };
45 |
46 | module.exports = attrs;
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kibana Tag Cloud Plugin
2 | A Tag Cloud Plugin for Kibana 4
3 |
4 | 
5 |
6 | This visualization was inspired by [Tim Roe's](https://www.timroes.de/) blog [post](https://www.timroes.de/2015/12/06/writing-kibana-4-plugins-visualizations-using-data/) on creating a tag cloud plugin for Kibana 4. It is built using [D3](d3js.org) and Jason Davie's [d3-cloud](https://github.com/jasondavies/d3-cloud) plugin.
7 |
8 | ### Requirements
9 | Kibana 4.3+
10 |
11 | ### Installation steps
12 | 1. Download and unpack [Kibana](https://www.elastic.co/downloads/kibana).
13 | 2. From the Kibana root directory, install the plugin with the following command:
14 |
15 | ```$ bin/kibana plugin -i tagcloud -u https://github.com/stormpython/tagcloud/archive/master.zip```
16 |
17 | ### Disclosure
18 | This repo is in its early stages. There is an outstanding [bug](https://github.com/stormpython/kibana-tag-cloud-plugin/issues/1) that needs to be fixed. In addition, please note d3-cloud's warning regarding how word clouds are rendered.
19 |
20 | >Note: if a word cannot be placed in any of the positions attempted along the spiral, it is not included in the final word layout. This may be addressed in a future release.
21 |
22 | ### Issues
23 | Please file issues [here](https://github.com/stormpython/kibana-tag-cloud-plugin/issues).
24 |
--------------------------------------------------------------------------------
/public/vis/index.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 | var control = require('plugins/tagcloud/vis/components/control/events');
4 | var layoutGenerator = require('plugins/tagcloud/vis/components/layout/generator');
5 | var chartGenerator = require('plugins/tagcloud/vis/components/visualization/generator');
6 |
7 | function vis() {
8 | var events = control();
9 | var layout = layoutGenerator();
10 | var chart = chartGenerator();
11 | var opts = {};
12 | var listeners = {};
13 | var size = [250, 250];
14 |
15 | function generator(selection) {
16 | selection.each(function (data) {
17 | events.listeners(listeners);
18 |
19 | layout.attr({
20 | type: opts.layout || 'grid',
21 | columns: opts.numOfColumns || 0,
22 | size: size
23 | });
24 |
25 | chart.options(opts);
26 |
27 | d3.select(this)
28 | .attr('width', '100%')
29 | .attr('height', size[1])
30 | .call(events)
31 | .call(layout)
32 | .selectAll('g.chart')
33 | .call(chart);
34 | });
35 | }
36 |
37 | // Public API
38 |
39 | generator.options = function (v) {
40 | if (!arguments.length) { return opts; }
41 | opts = _.isPlainObject(v) ? v : opts;
42 | return generator;
43 | };
44 |
45 | generator.listeners = function (v) {
46 | if (!arguments.length) { return listeners; }
47 | listeners = _.isPlainObject(v) ? v : listeners;
48 | return generator;
49 | };
50 |
51 | generator.size = function (v) {
52 | if (!arguments.length) { return size; }
53 | size = (_.isArray(v) && _.size(v) === 2) ? v : size;
54 | return generator;
55 | };
56 |
57 | return generator;
58 | }
59 |
60 | module.exports = vis;
61 |
--------------------------------------------------------------------------------
/public/cloud.js:
--------------------------------------------------------------------------------
1 | require('plugins/tagcloud/cloud.less');
2 | require('plugins/tagcloud/lib/cloud_controller.js');
3 | require('plugins/tagcloud/lib/cloud_directive.js');
4 |
5 | function TagCloudProvider(Private) {
6 | var TemplateVisType = Private(require('ui/template_vis_type/TemplateVisType'));
7 | var Schemas = Private(require('ui/Vis/Schemas'));
8 |
9 | return new TemplateVisType({
10 | name: 'tagcloud',
11 | title: 'Tag cloud',
12 | description: 'A tag cloud visualization is a visual representation of text data, ' +
13 | 'typically used to visualize free form text. Tags are usually single words, ' +
14 | 'and the importance of each tag is shown with font size or color.',
15 | icon: 'fa-cloud',
16 | template: require('plugins/tagcloud/cloud.html'),
17 | params: {
18 | defaults: {
19 | textScale: 'linear',
20 | orientations: 1,
21 | fromDegree: 0,
22 | toDegree: 0,
23 | font: 'serif',
24 | fontStyle: 'normal',
25 | fontWeight: 'normal',
26 | timeInterval: 500,
27 | spiral: 'archimedean',
28 | minFontSize: 18,
29 | maxFontSize: 72
30 | },
31 | editor: require('plugins/tagcloud/cloud_vis_params.html')
32 | },
33 | schemas: new Schemas([
34 | {
35 | group: 'metrics',
36 | name: 'metric',
37 | title: 'Tag Size',
38 | min: 1,
39 | max: 1,
40 | aggFilter: ['avg', 'sum', 'count', 'min', 'max', 'median', 'cardinality'],
41 | defaults: [
42 | { schema: 'metric', type: 'count' }
43 | ]
44 | },
45 | {
46 | group: 'buckets',
47 | name: 'segment',
48 | icon: 'fa fa-cloud',
49 | title: 'Tags',
50 | min: 1,
51 | max: 1,
52 | aggFilter: ['terms', 'significant_terms']
53 | }
54 | ])
55 | });
56 | }
57 |
58 | require('ui/registry/vis_types').register(TagCloudProvider);
59 |
--------------------------------------------------------------------------------
/public/lib/cloud_directive.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 | var visGenerator = require('plugins/tagcloud/vis/index');
4 |
5 | var module = require('ui/modules').get('tagcloud');
6 |
7 | module.directive('tagCloud', function () {
8 | function link (scope, element, attrs) {
9 | angular.element(document).ready(function () {
10 | var vis = visGenerator();
11 | var svg = d3.select(element[0]);
12 |
13 | function onSizeChange() {
14 | return {
15 | width: element.parent().width(),
16 | height: element.parent().height()
17 | };
18 | }
19 |
20 | function getSize() {
21 | var size = onSizeChange();
22 | return [size.width, size.height];
23 | };
24 |
25 | function render(data, opts, eventListeners) {
26 | opts = opts || {};
27 | eventListeners = eventListeners || {};
28 |
29 | vis.options(opts)
30 | .listeners(eventListeners)
31 | .size(getSize());
32 |
33 | if (data) {
34 | svg.datum(data).call(vis);
35 | }
36 | };
37 |
38 | scope.$watch('data', function (newVal, oldVal) {
39 | render(newVal, scope.options, scope.eventListeners);
40 | });
41 |
42 | scope.$watch('options', function (newVal, oldVal) {
43 | render(scope.data, newVal, scope.eventListeners);
44 | });
45 |
46 | scope.$watch('eventListeners', function (newVal, oldVal) {
47 | render(scope.data, scope.options, newVal);
48 | });
49 |
50 | scope.$watch(onSizeChange, _.debounce(function () {
51 | render(scope.data, scope.options, scope.eventListeners);
52 | }, 250), true);
53 |
54 | element.bind('resize', function () {
55 | scope.$apply();
56 | });
57 | });
58 | }
59 |
60 | return {
61 | restrict: 'E',
62 | scope: {
63 | data: '=',
64 | options: '=',
65 | eventListeners: '='
66 | },
67 | template: '',
68 | replace: 'true',
69 | link: link
70 | };
71 | });
72 |
--------------------------------------------------------------------------------
/public/cloud_vis_params.html:
--------------------------------------------------------------------------------
1 |
56 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var path = require('path');
3 | var path = require('path');
4 | var mkdirp = require('mkdirp');
5 | var Rsync = require('rsync');
6 | var Promise = require('bluebird');
7 | var eslint = require('gulp-eslint');
8 | var watch = require('gulp-watch');
9 |
10 | var pkg = require('./package.json');
11 |
12 | var kibanaPluginDir = path.resolve(__dirname, '../kibana/installedPlugins/tagcloud');
13 |
14 | var include = ['package.json', 'index.js', 'public', 'node_modules'];
15 | var exclude = Object.keys(pkg.devDependencies).map(function (name) {
16 | return path.join('node_modules', name);
17 | });
18 |
19 | function syncPluginTo(dest, done) {
20 | mkdirp(dest, function (err) {
21 | if (err) return done(err);
22 | Promise.all(include.map(function (name) {
23 | var source = path.resolve(__dirname, name);
24 | return new Promise(function (resolve, reject) {
25 | var rsync = new Rsync();
26 | rsync
27 | .source(source)
28 | .destination(dest)
29 | .flags('uav')
30 | .recursive(true)
31 | .set('delete')
32 | .exclude(exclude)
33 | .output(function (data) {
34 | process.stdout.write(data.toString('utf8'));
35 | });
36 | rsync.execute(function (err) {
37 | if (err) {
38 | console.log(err);
39 | return reject(err);
40 | }
41 | resolve();
42 | });
43 | });
44 | }))
45 | .then(function () {
46 | done();
47 | })
48 | .catch(done);
49 | });
50 | }
51 |
52 | gulp.task('sync', function (done) {
53 | syncPluginTo(kibanaPluginDir, done);
54 | });
55 |
56 | gulp.task('lint', function (done) {
57 | return gulp.src(['server/**/*.js', 'public/**/*.js', 'public/**/*.jsx'])
58 | // eslint() attaches the lint output to the eslint property
59 | // of the file object so it can be used by other modules.
60 | .pipe(eslint())
61 | // eslint.format() outputs the lint results to the console.
62 | // Alternatively use eslint.formatEach() (see Docs).
63 | .pipe(eslint.formatEach())
64 | // To have the process exit with an error code (1) on
65 | // lint error, return the stream and pipe to failOnError last.
66 | .pipe(eslint.failOnError());
67 | });
68 |
69 | const batch = require('gulp-batch');
70 |
71 | gulp.task('dev', ['sync'], function (done) {
72 | watch(['package.json', 'index.js', 'public/**/*', 'server/**/*'], batch(function(events, done) {
73 | gulp.start(['sync', 'lint'], done);
74 | }));
75 | });
76 |
--------------------------------------------------------------------------------
/public/vis/components/layout/layout.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 |
4 | function formatType(length, type, cols) {
5 | var output = {};
6 |
7 | switch (type) {
8 | case 'grid':
9 | output.rows = cols ? Math.ceil(length / cols) : Math.round(Math.sqrt(length));
10 | output.columns = cols || Math.ceil(Math.sqrt(length));
11 | break;
12 |
13 | case 'columns':
14 | output.rows = 1;
15 | output.columns = length;
16 | break;
17 |
18 | default:
19 | output.rows = length;
20 | output.columns = 1;
21 | break;
22 | }
23 |
24 | return output;
25 | }
26 |
27 | function baseLayout() {
28 | var type = 'grid'; // available types: 'rows', 'columns', 'grid'
29 | var size = [250, 250]; // [width, height]
30 | var rowScale = d3.scale.linear();
31 | var columnScale = d3.scale.linear();
32 | var numOfCols = 0;
33 |
34 | function layout(data) {
35 | var format = formatType(data.length, type, numOfCols);
36 | var rows = format.rows;
37 | var columns = format.columns;
38 | var cellWidth = size[0] / columns;
39 | var cellHeight = size[1] / rows;
40 | var cell = 0;
41 | var newData = [];
42 |
43 | rowScale.domain([0, rows]).range([0, size[1]]);
44 | columnScale.domain([0, columns]).range([0, size[0]]);
45 |
46 | d3.range(rows).forEach(function (row) {
47 | d3.range(columns).forEach(function (col) {
48 | var datum = data[cell];
49 | var obj = {
50 | dx: columnScale(col),
51 | dy: rowScale(row),
52 | width: cellWidth,
53 | height: cellHeight
54 | };
55 |
56 | function reduce(a, b) {
57 | a[b] = datum[b];
58 | return a;
59 | }
60 |
61 | if (!datum) { return; }
62 |
63 | // Do not mutate the original data, return a new object
64 | newData.push(Object.keys(datum).reduce(reduce, obj));
65 | cell += 1;
66 | });
67 | });
68 |
69 | return newData;
70 | }
71 |
72 | // Public API
73 | layout.type = function (v) {
74 | if (!arguments.length) { return type; }
75 | type = _.isString(v) ? v : type;
76 | return layout;
77 | };
78 |
79 | layout.columns = function (v) {
80 | if (!arguments.length) { return numOfCols; }
81 | numOfCols = _.isNumber(v) ? v : numOfCols;
82 | return layout;
83 | };
84 |
85 | layout.size = function (v) {
86 | if (!arguments.length) { return size; }
87 | size = (_.isArray(v) && _.size(v) === 2 && _.all(v, _.isNumber)) ? v : size;
88 | return layout;
89 | };
90 |
91 | return layout;
92 | }
93 |
94 | module.exports = baseLayout;
95 |
--------------------------------------------------------------------------------
/public/vis/components/elements/text.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var valuator = require('plugins/tagcloud/vis/components/utils/valuator');
3 |
4 | function textGenerator() {
5 | var colorScale = d3.scale.category20();
6 | var cssClass = 'tag';
7 | var transform = function (d) {
8 | return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')';
9 | };
10 | var fontSize = function (d) { return d.size + 'px'; };
11 | var fontFamily = function (d) { return d.font; };
12 | var fontWeight = function (d) { return d.weight; };
13 | var fontStyle = function (d) { return d.style; };
14 | var fill = function (d, i) { return colorScale(i); };
15 | var fillOpacity = d3.functor(1);
16 | var textAnchor = d3.functor('middle');
17 | var textAccessor = function (d) { return d.text; };
18 |
19 | function generator(selection) {
20 | selection.each(function (data, index) {
21 | var text = d3.select(this).selectAll('text')
22 | .data(data);
23 |
24 | text.exit().remove();
25 |
26 | text.enter().append('text')
27 | .attr('class', cssClass);
28 |
29 | text
30 | .attr('transform', transform)
31 | .attr('text-anchor', textAnchor)
32 | .style('fill', fill)
33 | .style('fill-opacity', fillOpacity)
34 | .style('font-size', fontSize)
35 | .style('font-family', fontFamily)
36 | .style('font-weight', fontWeight)
37 | .style('font-style', fontStyle)
38 | .text(textAccessor);
39 | });
40 | }
41 |
42 | // Public API
43 | generator.cssClass = function (v) {
44 | if (!arguments.length) { return cssClass; }
45 | cssClass = d3.functor(v);
46 | return generator;
47 | };
48 |
49 | generator.transform = function (v) {
50 | if (!arguments.length) { return transform; }
51 | transform = d3.functor(v);
52 | return generator;
53 | };
54 |
55 | generator.fill = function (v) {
56 | if (!arguments.length) { return fill; }
57 | fill = d3.functor(v);
58 | return generator;
59 | };
60 |
61 | generator.fillOpacity = function (v) {
62 | if (!arguments.length) { return fillOpacity; }
63 | fillOpacity = d3.functor(v);
64 | return generator;
65 | };
66 |
67 | generator.fontFamily = function (v) {
68 | if (!arguments.length) { return fontFamily; }
69 | fontFamily = d3.functor(v);
70 | return generator;
71 | };
72 |
73 | generator.fontSize = function (v) {
74 | if (!arguments.length) { return fontSize; }
75 | fontSize = d3.functor(v);
76 | return generator;
77 | };
78 |
79 | generator.fontStyle = function (v) {
80 | if (!arguments.length) { return fontStyle; }
81 | fontStyle = d3.functor(v);
82 | return generator;
83 | };
84 |
85 | generator.fontWeight = function (v) {
86 | if (!arguments.length) { return fontWeight; }
87 | fontSize = d3.functor(v);
88 | return generator;
89 | };
90 | generator.textAnchor = function (v) {
91 | if (!arguments.length) { return textAnchor; }
92 | textAnchor = d3.functor(v);
93 | return generator;
94 | };
95 |
96 | generator.text = function (v) {
97 | if (!arguments.length) { return textAccessor; }
98 | textAccessor = valuator(v);
99 | return generator;
100 | };
101 |
102 | return generator;
103 | }
104 |
105 | module.exports = textGenerator;
106 |
--------------------------------------------------------------------------------
/public/vis/components/d3.layout.cloud/d3-dispatch.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3 | typeof define === 'function' && define.amd ? define('d3-dispatch', ['exports'], factory) :
4 | factory((global.d3_dispatch = {}));
5 | }(this, function (exports) { 'use strict';
6 |
7 | function dispatch() {
8 | return new Dispatch(arguments);
9 | }
10 |
11 | function Dispatch(types) {
12 | var i = -1,
13 | n = types.length,
14 | callbacksByType = {},
15 | callbackByName = {},
16 | type,
17 | that = this;
18 |
19 | that.on = function(type, callback) {
20 | type = parseType(type);
21 |
22 | // Return the current callback, if any.
23 | if (arguments.length < 2) {
24 | return (callback = callbackByName[type.name]) && callback.value;
25 | }
26 |
27 | // If a type was specified…
28 | if (type.type) {
29 | var callbacks = callbacksByType[type.type],
30 | callback0 = callbackByName[type.name],
31 | i;
32 |
33 | // Remove the current callback, if any, using copy-on-remove.
34 | if (callback0) {
35 | callback0.value = null;
36 | i = callbacks.indexOf(callback0);
37 | callbacksByType[type.type] = callbacks = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
38 | delete callbackByName[type.name];
39 | }
40 |
41 | // Add the new callback, if any.
42 | if (callback) {
43 | callback = {value: callback};
44 | callbackByName[type.name] = callback;
45 | callbacks.push(callback);
46 | }
47 | }
48 |
49 | // Otherwise, if a null callback was specified, remove all callbacks with the given name.
50 | else if (callback == null) {
51 | for (var otherType in callbacksByType) {
52 | if (callback = callbackByName[otherType + type.name]) {
53 | callback.value = null;
54 | var callbacks = callbacksByType[otherType], i = callbacks.indexOf(callback);
55 | callbacksByType[otherType] = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
56 | delete callbackByName[callback.name];
57 | }
58 | }
59 | }
60 |
61 | return that;
62 | };
63 |
64 | while (++i < n) {
65 | type = types[i] + "";
66 | if (!type || (type in that)) throw new Error("illegal or duplicate type: " + type);
67 | callbacksByType[type] = [];
68 | that[type] = applier(type);
69 | }
70 |
71 | function parseType(type) {
72 | var i = (type += "").indexOf("."), name = type;
73 | if (i >= 0) type = type.slice(0, i); else name += ".";
74 | if (type && !callbacksByType.hasOwnProperty(type)) throw new Error("unknown type: " + type);
75 | return {type: type, name: name};
76 | }
77 |
78 | function applier(type) {
79 | return function() {
80 | var callbacks = callbacksByType[type], // Defensive reference; copy-on-remove.
81 | callback,
82 | callbackValue,
83 | i = -1,
84 | n = callbacks.length;
85 |
86 | while (++i < n) {
87 | if (callbackValue = (callback = callbacks[i]).value) {
88 | callbackValue.apply(this, arguments);
89 | }
90 | }
91 |
92 | return that;
93 | };
94 | }
95 | }
96 |
97 | dispatch.prototype = Dispatch.prototype;
98 |
99 | var version = "0.2.5";
100 |
101 | exports.version = version;
102 | exports.dispatch = dispatch;
103 |
104 | }));
105 |
--------------------------------------------------------------------------------
/public/vis/components/visualization/tag_cloud.js:
--------------------------------------------------------------------------------
1 | var d3 = require('d3');
2 | var _ = require('lodash');
3 | var layoutCloud = require('plugins/tagcloud/vis/components/d3.layout.cloud/d3.layout.cloud');
4 | var gGenerator = require('plugins/tagcloud/vis/components/elements/g');
5 | var textElement = require('plugins/tagcloud/vis/components/elements/text');
6 | var valuator = require('plugins/tagcloud/vis/components/utils/valuator');
7 |
8 | function tagCloud() {
9 | var textScale = d3.scale.linear();
10 | var accessor = function (d) { return d; };
11 | var colorScale = d3.scale.category20();
12 | var fontNormal = d3.functor('normal');
13 | var width = 250;
14 | var height = 250;
15 | var rotationScale = d3.scale.linear();
16 | var orientations = 1;
17 | var fromDegree = 0;
18 | var toDegree = 0;
19 | var font = d3.functor('serif');
20 | var fontSize = function (d) { return textScale(d.size); };
21 | var fontStyle = fontNormal;
22 | var fontWeight = fontNormal;
23 | var minFontSize = 12;
24 | var maxFontSize = 60;
25 | var timeInterval = Infinity;
26 | var spiral = 'archimedean';
27 | var padding = 1;
28 | var textAccessor = function (d) { return d.text; };
29 | var fill = function (d, i) { return colorScale(d.text); };
30 | var fillOpacity = d3.functor(1);
31 | var textAnchor = d3.functor('middle');
32 | var textClass = d3.functor('tag');
33 |
34 | function getSize(d) {
35 | return d.size;
36 | }
37 |
38 | function generator(selection) {
39 | selection.each(function (data, index) {
40 | var tags = accessor.call(this, data, index);
41 |
42 | var text = textElement()
43 | .cssClass(textClass)
44 | .fontSize(function (d) { return d.size + 'px'; })
45 | .fill(fill)
46 | .fillOpacity(fillOpacity)
47 | .textAnchor(textAnchor);
48 |
49 | var group = gGenerator()
50 | .cssClass('tags')
51 | .transform('translate(' + (width / 2) + ',' + (height / 2) + ')');
52 |
53 | var g = d3.select(this)
54 | .datum([data])
55 | .call(group);
56 |
57 | var numOfOrientations = orientations - 1;
58 |
59 | rotationScale
60 | .domain([0, numOfOrientations])
61 | .range([fromDegree, toDegree]);
62 |
63 | textScale
64 | .domain(d3.extent(tags, getSize))
65 | .range([minFontSize, maxFontSize]);
66 |
67 | function draw(tags) {
68 | g.select('g.' + group.cssClass())
69 | .datum(tags)
70 | .call(text);
71 | }
72 |
73 | layoutCloud()
74 | .size([width, height])
75 | .words(tags)
76 | .text(textAccessor)
77 | .rotate(function() {
78 | return rotationScale(~~(Math.random() * numOfOrientations));
79 | })
80 | .font(font)
81 | .fontStyle(fontStyle)
82 | .fontWeight(fontWeight)
83 | .fontSize(fontSize)
84 | .timeInterval(timeInterval)
85 | .spiral(spiral)
86 | .padding(padding)
87 | .on('end', draw)
88 | .start();
89 | });
90 | }
91 |
92 | // Public API
93 | generator.accessor = function (v) {
94 | if (!arguments.length) { return accessor; }
95 | accessor = valuator(v);
96 | return generator;
97 | };
98 |
99 | generator.width = function (v) {
100 | if (!arguments.length) { return width; }
101 | width = v;
102 | return generator;
103 | };
104 |
105 | generator.height = function (v) {
106 | if (!arguments.length) { return height; }
107 | height = v;
108 | return generator;
109 | };
110 |
111 | generator.orientations = function (v) {
112 | if (!arguments.length) { return orientations; }
113 | orientations = v;
114 | return generator;
115 | };
116 |
117 | generator.fromDegree = function (v) {
118 | if (!arguments.length) { return fromDegree; }
119 | fromDegree = v;
120 | return generator;
121 | };
122 |
123 | generator.toDegree = function (v) {
124 | if (!arguments.length) { return toDegree; }
125 | toDegree = v;
126 | return generator;
127 | };
128 |
129 | generator.font = function (v) {
130 | if (!arguments.length) { return font; }
131 | font = v;
132 | return generator;
133 | };
134 |
135 | generator.fontSize = function (v) {
136 | if (!arguments.length) { return fontSize; }
137 | fontSize = v;
138 | return generator;
139 | };
140 |
141 | generator.fontStyle = function (v) {
142 | if (!arguments.length) { return fontStyle; }
143 | fontStyle = v;
144 | return generator;
145 | };
146 |
147 | generator.fontWeight = function (v) {
148 | if (!arguments.length) { return fontWeight; }
149 | fontWeight = v;
150 | return generator;
151 | };
152 |
153 | generator.minFontSize = function (v) {
154 | if (!arguments.length) { return minFontSize; }
155 | minFontSize = v;
156 | return generator;
157 | };
158 |
159 | generator.maxFontSize = function (v) {
160 | if (!arguments.length) { return maxFontSize; }
161 | maxFontSize = v;
162 | return generator;
163 | };
164 |
165 | generator.timeInterval = function (v) {
166 | if (!arguments.length) { return timeInterval; }
167 | timeInterval = v;
168 | return generator;
169 | };
170 |
171 | generator.spiral = function (v) {
172 | if (!arguments.length) { return spiral; }
173 | spiral = v;
174 | return generator;
175 | };
176 |
177 | generator.padding = function (v) {
178 | if (!arguments.length) { return padding; }
179 | padding = v;
180 | return generator;
181 | };
182 |
183 | generator.text = function (v) {
184 | if (!arguments.length) { return textAccessor; }
185 | textAccessor = v;
186 | return generator;
187 | };
188 |
189 | generator.textScale = function (v) {
190 | var scales = ['linear', 'log', 'sqrt'];
191 | if (!arguments.length) { return textScale; }
192 | textScale = _.includes(scales, v) ? d3.scale[v]() : textScale;
193 | return generator;
194 | };
195 |
196 | generator.fill = function (v) {
197 | if (!arguments.length) { return fill; }
198 | fill = v;
199 | return generator;
200 | };
201 |
202 | generator.fillOpacity = function (v) {
203 | if (!arguments.length) { return fillOpacity; }
204 | fillOpacity = v;
205 | return generator;
206 | };
207 |
208 | generator.textAnchor = function (v) {
209 | if (!arguments.length) { return textAnchor; }
210 | textAnchor = v;
211 | return generator;
212 | };
213 |
214 | generator.textClass = function (v) {
215 | if (!arguments.length) { return textClass; }
216 | textClass = v;
217 | return generator;
218 | };
219 |
220 | return generator;
221 | }
222 |
223 | module.exports = tagCloud;
224 |
--------------------------------------------------------------------------------
/public/vis/components/d3.layout.cloud/d3.layout.cloud.js:
--------------------------------------------------------------------------------
1 | // Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
2 | // Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
3 |
4 | var dispatch = require('plugins/tagcloud/vis/components/d3.layout.cloud/d3-dispatch').dispatch;
5 |
6 | var cloudRadians = Math.PI / 180,
7 | cw = 1 << 11 >> 5,
8 | ch = 1 << 11;
9 |
10 | module.exports = function() {
11 | var size = [256, 256],
12 | text = cloudText,
13 | font = cloudFont,
14 | fontSize = cloudFontSize,
15 | fontStyle = cloudFontNormal,
16 | fontWeight = cloudFontNormal,
17 | rotate = cloudRotate,
18 | padding = cloudPadding,
19 | spiral = archimedeanSpiral,
20 | words = [],
21 | timeInterval = Infinity,
22 | event = dispatch("word", "end"),
23 | timer = null,
24 | random = Math.random,
25 | cloud = {},
26 | canvas = cloudCanvas;
27 |
28 | cloud.canvas = function(_) {
29 | return arguments.length ? (canvas = functor(_), cloud) : canvas;
30 | };
31 |
32 | cloud.start = function() {
33 | var contextAndRatio = getContext(canvas()),
34 | board = zeroArray((size[0] >> 5) * size[1]),
35 | bounds = null,
36 | n = words.length,
37 | i = -1,
38 | tags = [],
39 | data = words.map(function(d, i) {
40 | d.text = text.call(this, d, i);
41 | d.font = font.call(this, d, i);
42 | d.style = fontStyle.call(this, d, i);
43 | d.weight = fontWeight.call(this, d, i);
44 | d.rotate = rotate.call(this, d, i);
45 | d.size = ~~fontSize.call(this, d, i);
46 | d.padding = padding.call(this, d, i);
47 | return d;
48 | }).sort(function(a, b) { return b.size - a.size; });
49 |
50 | if (timer) clearInterval(timer);
51 | timer = setInterval(step, 0);
52 | step();
53 |
54 | return cloud;
55 |
56 | function step() {
57 | var start = Date.now();
58 | while (Date.now() - start < timeInterval && ++i < n && timer) {
59 | var d = data[i];
60 | d.x = (size[0] * (random() + .5)) >> 1;
61 | d.y = (size[1] * (random() + .5)) >> 1;
62 | cloudSprite(contextAndRatio, d, data, i);
63 | if (d.hasText && place(board, d, bounds)) {
64 | tags.push(d);
65 | event.word(d);
66 | if (bounds) cloudBounds(bounds, d);
67 | else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
68 | // Temporary hack
69 | d.x -= size[0] >> 1;
70 | d.y -= size[1] >> 1;
71 | }
72 | }
73 | if (i >= n) {
74 | cloud.stop();
75 | event.end(tags, bounds);
76 | }
77 | }
78 | }
79 |
80 | cloud.stop = function() {
81 | if (timer) {
82 | clearInterval(timer);
83 | timer = null;
84 | }
85 | return cloud;
86 | };
87 |
88 | function getContext(canvas) {
89 | canvas.width = canvas.height = 1;
90 | var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
91 | canvas.width = (cw << 5) / ratio;
92 | canvas.height = ch / ratio;
93 |
94 | var context = canvas.getContext("2d");
95 | context.fillStyle = context.strokeStyle = "red";
96 | context.textAlign = "center";
97 |
98 | return {context: context, ratio: ratio};
99 | }
100 |
101 | function place(board, tag, bounds) {
102 | var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
103 | startX = tag.x,
104 | startY = tag.y,
105 | maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
106 | s = spiral(size),
107 | dt = random() < .5 ? 1 : -1,
108 | t = -dt,
109 | dxdy,
110 | dx,
111 | dy;
112 |
113 | while (dxdy = s(t += dt)) {
114 | dx = ~~dxdy[0];
115 | dy = ~~dxdy[1];
116 |
117 | if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
118 |
119 | tag.x = startX + dx;
120 | tag.y = startY + dy;
121 |
122 | if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
123 | tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
124 | // TODO only check for collisions within current bounds.
125 | if (!bounds || !cloudCollide(tag, board, size[0])) {
126 | if (!bounds || collideRects(tag, bounds)) {
127 | var sprite = tag.sprite,
128 | w = tag.width >> 5,
129 | sw = size[0] >> 5,
130 | lx = tag.x - (w << 4),
131 | sx = lx & 0x7f,
132 | msx = 32 - sx,
133 | h = tag.y1 - tag.y0,
134 | x = (tag.y + tag.y0) * sw + (lx >> 5),
135 | last;
136 | for (var j = 0; j < h; j++) {
137 | last = 0;
138 | for (var i = 0; i <= w; i++) {
139 | board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
140 | }
141 | x += sw;
142 | }
143 | delete tag.sprite;
144 | return true;
145 | }
146 | }
147 | }
148 | return false;
149 | }
150 |
151 | cloud.timeInterval = function(_) {
152 | return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
153 | };
154 |
155 | cloud.words = function(_) {
156 | return arguments.length ? (words = _, cloud) : words;
157 | };
158 |
159 | cloud.size = function(_) {
160 | return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
161 | };
162 |
163 | cloud.font = function(_) {
164 | return arguments.length ? (font = functor(_), cloud) : font;
165 | };
166 |
167 | cloud.fontStyle = function(_) {
168 | return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
169 | };
170 |
171 | cloud.fontWeight = function(_) {
172 | return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
173 | };
174 |
175 | cloud.rotate = function(_) {
176 | return arguments.length ? (rotate = functor(_), cloud) : rotate;
177 | };
178 |
179 | cloud.text = function(_) {
180 | return arguments.length ? (text = functor(_), cloud) : text;
181 | };
182 |
183 | cloud.spiral = function(_) {
184 | return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
185 | };
186 |
187 | cloud.fontSize = function(_) {
188 | return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
189 | };
190 |
191 | cloud.padding = function(_) {
192 | return arguments.length ? (padding = functor(_), cloud) : padding;
193 | };
194 |
195 | cloud.random = function(_) {
196 | return arguments.length ? (random = _, cloud) : random;
197 | };
198 |
199 | cloud.on = function() {
200 | var value = event.on.apply(event, arguments);
201 | return value === event ? cloud : value;
202 | };
203 |
204 | return cloud;
205 | };
206 |
207 | function cloudText(d) {
208 | return d.text;
209 | }
210 |
211 | function cloudFont() {
212 | return "serif";
213 | }
214 |
215 | function cloudFontNormal() {
216 | return "normal";
217 | }
218 |
219 | function cloudFontSize(d) {
220 | return Math.sqrt(d.value);
221 | }
222 |
223 | function cloudRotate() {
224 | return (~~(Math.random() * 6) - 3) * 30;
225 | }
226 |
227 | function cloudPadding() {
228 | return 1;
229 | }
230 |
231 | // Fetches a monochrome sprite bitmap for the specified text.
232 | // Load in batches for speed.
233 | function cloudSprite(contextAndRatio, d, data, di) {
234 | if (d.sprite) return;
235 | var c = contextAndRatio.context,
236 | ratio = contextAndRatio.ratio;
237 |
238 | c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
239 | var x = 0,
240 | y = 0,
241 | maxh = 0,
242 | n = data.length;
243 | --di;
244 | while (++di < n) {
245 | d = data[di];
246 | c.save();
247 | c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
248 | var w = c.measureText(d.text + "m").width * ratio,
249 | h = d.size << 1;
250 | if (d.rotate) {
251 | var sr = Math.sin(d.rotate * cloudRadians),
252 | cr = Math.cos(d.rotate * cloudRadians),
253 | wcr = w * cr,
254 | wsr = w * sr,
255 | hcr = h * cr,
256 | hsr = h * sr;
257 | w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
258 | h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
259 | } else {
260 | w = (w + 0x1f) >> 5 << 5;
261 | }
262 | if (h > maxh) maxh = h;
263 | if (x + w >= (cw << 5)) {
264 | x = 0;
265 | y += maxh;
266 | maxh = 0;
267 | }
268 | if (y + h >= ch) break;
269 | c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
270 | if (d.rotate) c.rotate(d.rotate * cloudRadians);
271 | c.fillText(d.text, 0, 0);
272 | if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
273 | c.restore();
274 | d.width = w;
275 | d.height = h;
276 | d.xoff = x;
277 | d.yoff = y;
278 | d.x1 = w >> 1;
279 | d.y1 = h >> 1;
280 | d.x0 = -d.x1;
281 | d.y0 = -d.y1;
282 | d.hasText = true;
283 | x += w;
284 | }
285 | var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
286 | sprite = [];
287 | while (--di >= 0) {
288 | d = data[di];
289 | if (!d.hasText) continue;
290 | var w = d.width,
291 | w32 = w >> 5,
292 | h = d.y1 - d.y0;
293 | // Zero the buffer
294 | for (var i = 0; i < h * w32; i++) sprite[i] = 0;
295 | x = d.xoff;
296 | if (x == null) return;
297 | y = d.yoff;
298 | var seen = 0,
299 | seenRow = -1;
300 | for (var j = 0; j < h; j++) {
301 | for (var i = 0; i < w; i++) {
302 | var k = w32 * j + (i >> 5),
303 | m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
304 | sprite[k] |= m;
305 | seen |= m;
306 | }
307 | if (seen) seenRow = j;
308 | else {
309 | d.y0++;
310 | h--;
311 | j--;
312 | y++;
313 | }
314 | }
315 | d.y1 = d.y0 + seenRow;
316 | d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
317 | }
318 | }
319 |
320 | // Use mask-based collision detection.
321 | function cloudCollide(tag, board, sw) {
322 | sw >>= 5;
323 | var sprite = tag.sprite,
324 | w = tag.width >> 5,
325 | lx = tag.x - (w << 4),
326 | sx = lx & 0x7f,
327 | msx = 32 - sx,
328 | h = tag.y1 - tag.y0,
329 | x = (tag.y + tag.y0) * sw + (lx >> 5),
330 | last;
331 | for (var j = 0; j < h; j++) {
332 | last = 0;
333 | for (var i = 0; i <= w; i++) {
334 | if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
335 | & board[x + i]) return true;
336 | }
337 | x += sw;
338 | }
339 | return false;
340 | }
341 |
342 | function cloudBounds(bounds, d) {
343 | var b0 = bounds[0],
344 | b1 = bounds[1];
345 | if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
346 | if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
347 | if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
348 | if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
349 | }
350 |
351 | function collideRects(a, b) {
352 | 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;
353 | }
354 |
355 | function archimedeanSpiral(size) {
356 | var e = size[0] / size[1];
357 | return function(t) {
358 | return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
359 | };
360 | }
361 |
362 | function rectangularSpiral(size) {
363 | var dy = 4,
364 | dx = dy * size[0] / size[1],
365 | x = 0,
366 | y = 0;
367 | return function(t) {
368 | var sign = t < 0 ? -1 : 1;
369 | // See triangular numbers: T_n = n * (n + 1) / 2.
370 | switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
371 | case 0: x += dx; break;
372 | case 1: y += dy; break;
373 | case 2: x -= dx; break;
374 | default: y -= dy; break;
375 | }
376 | return [x, y];
377 | };
378 | }
379 |
380 | // TODO reuse arrays?
381 | function zeroArray(n) {
382 | var a = [],
383 | i = -1;
384 | while (++i < n) a[i] = 0;
385 | return a;
386 | }
387 |
388 | function cloudCanvas() {
389 | return document.createElement("canvas");
390 | }
391 |
392 | function functor(d) {
393 | return typeof d === "function" ? d : function() { return d; };
394 | }
395 |
396 | var spirals = {
397 | archimedean: archimedeanSpiral,
398 | rectangular: rectangularSpiral
399 | };
400 |
--------------------------------------------------------------------------------