├── .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 | ![Kibana Tag Cloud](tagcloud.png) 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 |
2 |
3 | 4 | 8 |
9 | 10 |
11 | 12 | 17 |
18 | 19 |
20 | 21 | 26 |
27 | 28 |
29 | 30 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
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 | --------------------------------------------------------------------------------