cloudvisio
Version:
Visual Representation of cloud data
1,970 lines (1,712 loc) • 386 kB
JavaScript
;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
// dependencies
var d3 = require('d3');
// force chart
var force = function( self ) {
this.self = self;
// setup options
self.set( this.defaults );
// set axis
self._axisSchema( this.schema );
};
force.prototype = {
layout: "force",
schema: {
label: "string",
group: "number",
radius: "number"
},
defaults: {
charge: -20,
distance: 30,
ir : 0,
radius: 5
},
constructor: force,
render: function( append ){
var self = this.self;
var that = this;
var svg = d3.select( self.el + " "+ self.options.container);
var width = 4000,
height = 3000;
var data = this.nodes = this.data();
var force = d3.layout.force()
.charge( self.options.charge )
.linkDistance( self.options.distance )
.size([width, height])
.nodes( data )
.links([])
.start();
/*
var groups = d3.nest().key(function(d) { return d.group & 3; }).entries( nodes );
var groupPath = function(d) {
return "M" + d3.geom.hull(d.values.map(function(i) { return [i.x, i.y]; })).join("L") + "Z";
};
var groupFill = function(d, i) { return self.color(i & 3); };
*/
this.node = svg.selectAll(".node")
.data( data ).enter()
.append("circle")
.attr("class", "node")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return self.color( d.group ); })
.call(force.drag);
this.node.append("title")
.text(function(d) { return d.name; });
force.on("tick", function(e){ that.update(e); });
},
// local
data: function(){
var self = this.self,
d = self.models;
// get keys
//var keys = self.keys( d );
// required data:
// - name (string)
var name = self._axis.label,
group = self._axis.group,
radius = self._axis.radius;
// old code:
// - group (integer)
/*
if( keys.indexOf("id") > -1 ) name = "id";
if( keys.indexOf("title") > -1 ) name = "title";
if( keys.indexOf("label") > -1 ) name = "label";
if( keys.indexOf("name") > -1 ) name = "name";
for( var i in keys ){
if( keys[i].search(/group_/gi) === 0 ) group = keys[i];
}
*/
//
var result = [];
for( var j in d ){
result.push({
name : d[j][name],
group : d[j][group],
// if radius is a number, use it as a static value
radius: ( isNaN(radius) )? d[j][radius] : radius
});
}
//
return result;
},
update: function(e) {
// Push different nodes in different directions for clustering.
var k = 6 * e.alpha;
this.nodes.forEach(function(o, i) {
//o.x += i & 2 ? k : -k;
//o.y += i & 1 ? k : -k;
o.x += (o.group + 2) & 2 ? k : -k;
o.y += (o.group + 2) & 1 ? k : -k;
});
var q = d3.geom.quadtree( this.nodes ),
i = 0,
n = this.nodes.length;
while (++i < n) {
q.visit( this.collide( this.nodes[i] ));
}
this.node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
/*
svg.selectAll("path")
.data(groups)
.attr("d", groupPath)
.enter().insert("path", "circle")
.style("fill", groupFill)
.style("stroke", groupFill)
.style("stroke-width", 40)
.style("stroke-linejoin", "round")
.style("opacity", .2)
.attr("d", groupPath);
*/
},
// Collision detection
// Source: https://gist.github.com/GerHobbelt/3116713
collide: function(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
};
module.exports = force;
},{"d3":18}],2:[function(require,module,exports){
// dependencies
var d3 = require('d3'),
arc = d3.svg.arc();
var pie = function( self ) {
this.self = self;
// setup options
self.set( this.defaults );
// set axis
self._axisSchema( this.schema );
};
pie.prototype = {
layout: "pie",
schema: {
label: "string",
value: "number"
},
defaults: {
r : 1500, // radius, height/2
ir : 0,
textOffset: 500
},
constructor: pie,
render: function( append ){
var width = 4000, // internal (non-customizable) width
height = 3000, // internal (non-customizable) height
self = this.self,
data = this.data();
// Insert an svg:svg element (with margin) for each row in our dataset. A
// child svg:g element translates the origin to the pie center.
var svg = d3.select( self.el + " "+ self.options.container)
.data( data ) //associate our data with the document
.append("svg:g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
// add pie group
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) { return d.value; }) // Binding each value to the pie
.sort( function(d) { return null; } );
// Select all <g> elements with class slice (there aren't any yet)
var arcs = svg.selectAll("g.slice")
// Associate the generated pie data (an array of arcs, each having startAngle,
// endAngle and value properties)
.data(pie)
// This will create <g> elements for every "extra" data element that should be associated
// with a selection. The result is creating a <g> for every object in the data array
.enter()
// Create a group to hold each slice (we will have a <path> and a <text>
// element associated with each slice)
.append("svg:g")
.attr("class", "slice"); //allow us to style things in the slices (like text)
// The data for each svg:svg element is a row of numbers (an array). We pass
// that to d3.layout.pie to compute the angles for each arc. These start and end
// angles are passed to d3.svg.arc to draw arcs! Note that the arc radius is
// specified on the arc, not the layout.
var slices = arcs.append("svg:path")
.attr("alt", function(d) { return d.data.label; })
.attr("d", arc
.innerRadius( self.options.ir )
.outerRadius( self.options.r )
)
.style("fill", function(d, i) { return self.color( i ); });
// animate slices
slices.transition()
.ease("cubic")
.duration(2000)
.attrTween("d", this.tweenPie);
var label = arcs.append("svg:text")
.style("font-size", function(d) { return Math.max( 1, (d.data.value/100) ) * 15; }) //add a label to each slice
.attr("fill", "white")
.attr("transform", function(d, i) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.innerRadius = self.options.r/2;
d.outerRadius = self.options.r;
return "translate(" + arc.centroid(d) + ")"; //this gives us a pair of coordinates like [50, 50]
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) { return d.data.label; }) //get the label from our original data array
.style("opacity", 0)
.transition().ease("cubic").duration(2000).style("opacity", 1);
//.transition().ease("cubic").duration(2000).attrTween("opacity", d3.interpolateNumber(0, 100) );
},
data: function (){
var self = this.self,
data = [];
var label = self._axis.label,
value = self._axis.value;
data = self.models.map(function( data, i ){
return { label: data[label], value: data[value] };
});
return [data];
},
// helpers
tweenPie: function(b) {
b.innerRadius = 0;
var i = d3.interpolate({startAngle: 0, endAngle: 0}, b);
return function(t) {
return arc(i(t));
};
}
};
module.exports = pie;
},{"d3":18}],3:[function(require,module,exports){
// dependencies
var d3 = require('d3');
// stacked/bar chart
var stack = function( self ) {
this.self = self;
// setup options
self.set( this.defaults );
// set axis
//self._axisSchema( this.schema );
};
stack.prototype = {
layout: "stack",
schema: {
x: "number",
y: "number",
//text: "string" // optional
},
defaults: {
},
constructor: stack,
render: function( append ){
var self = this.self;
var width = 4000,
height = 3000;
var nodes = this.data();
var svg = d3.select( self.el + " "+ self.options.container)
.append("svg:g")
.attr("transform", "translate(0,3000)");
x = d3.scale.ordinal().rangeRoundBands([0, width]);
y = d3.scale.linear().range([0, height]);
var data = d3.layout.stack()( nodes.values );
x.domain(data[0].map(function(d) { return d.x; }));
y.domain([0, d3.max(data[data.length - 1], function(d) { return d.y0 + d.y; })]);
// Add a group for each column.
var valgroup = svg.selectAll("g.valgroup")
.data( data )
.enter().append("svg:g")
.attr("class", "valgroup")
.style("fill", function(d, i) { return self.color(i); })
.style("stroke", function(d, i) { return d3.rgb( self.color(i)).darker(); })
.style("stroke-width", "5px");
// Add a rect for each entry.
var rect = valgroup.selectAll("rect")
.data(function(d){return d;})
.enter().append("svg:rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return -y(d.y0) - y(d.y); })
.attr("height", function(d) { return y(d.y); })
.attr("width", x.rangeBand() )
.append("title")
.text(function(d) { return d.text; });
//parse = d3.time.format("%m/%Y").parse,
//format = d3.time.format("%b");
// Add a label per axis
svg.selectAll("text")
.data( nodes.labels )
.enter().append("svg:text")
.attr("x", function(d) {
if( nodes.labels[0] == d ){
// first is x, horizontal
return (x.range().length / 2) * x.rangeBand() ; // width /2
} else {
// second is y, vertical
return - (height/2);
}
})
.attr("y", "-50")
.attr("transform" , function(d) {
if( nodes.labels[0] == d ){
// first is x, horizontal
return "rotate(0)";
} else {
// second is y, vertical
return "rotate(90)";
}
})
.attr("text-anchor", "middle")
.attr("dy", "-100px")
.attr("font-size", "150")
.style("fill-opacity", "0.6")
.text(function(d) { return d; });
// Add y-axis rules.
var rule = svg.selectAll("g.rule")
.data(y.ticks(5))
.enter().append("svg:g")
.attr("class", "rule")
.attr("transform", function(d) { return "translate(0," + -y(d) + ")"; });
rule.append("svg:line")
.attr("x2", width)
.style("stroke", function(d) { return d ? "#fff" : "#000"; })
.style("stroke-width", "5px")
.style("stroke-opacity", function(d) { return d ? 0.3 : null; });
rule.append("svg:text")
.attr("x", "40px")
.attr("dy", "-20px")
.attr("font-size", "50")
.attr("fill", "#fff")
.style("stroke", "#ccc")
.style("stroke-width", "2px")
//.text(function(d) { console.log("d", d);return nodes.values[d].text; });
.text(d3.format(",d"));
/* .attr("x", function(d) { return x(d) + x.rangeBand() / 2; }) */
},
data: function(){
var self = this.self;
var labels = [], values = [], x;
// check for the x axis
if( self._axis.x ){
x = self._axis.x;
// add label
labels.push( x );
}
// loop through axis
for( var key in self._axis ){
var label = self._axis[key];
// the first axis is the x axis (unless it exists)
if( !x ){
x = label;
// add label
labels.push( label );
continue;
}
if( key == "x" ) continue;
// add label
labels.push( label );
// normalize data
var data = self.models.map(function( item, i ){
return { x : ( typeof item[x] == "number" ) ? item[x] : i, y : parseInt( item[label], 10 ), text: item[x] };
});
values.push( data );
}
// in a stacked bar there's more than one passes from the models
return {
labels : labels,
values : values
};
}
};
module.exports = stack;
},{"d3":18}],4:[function(require,module,exports){
var d3 = require('d3');
module.exports = {
el: "#vis",
layout: "stack",
container: "svg",
width: "100%",
height: "100%",
// The color scale will be assigned by index, but if you define your data using objects, you could pass
// in a named field from the data object instead, such as `d.name`. Colors are assigned lazily,
// so if you want deterministic behavior, define a domain for the color scale.
colors: d3.scale.category20c(),
//colors: d3.scale.ordinal().range(["darkblue", "blue", "lightblue"]);
renderErrors: false
};
},{"d3":18}],5:[function(require,module,exports){
// @name cloudvisio
// @url https://github.com/makesites/cloudvisio
// @author makesites
// @license Apache License, Version 2.0
module.exports = require('./lib/main');
},{"./lib/main":11}],6:[function(require,module,exports){
(function(){var utils = require('./utils');
var axis = {
_axisSchema: function( schema ){
// reset axis (conditionally)
//this._axis = {};
// remove old axis
for(var i in this._axis){
if(typeof schema[i] == "undefined") delete this._axis[i];
}
// add new axis
for(var j in schema){
// get the default value ( if set )
var value = ( this.options[j] ) ? this.options[j] : false;
// preserve existing axis
if(typeof this._axis[j] == "undefined") this._axis[j] = value;
}
},
// make an intelligent decision on what the next axis may be
_findAxis: function( type ){
// get the keys of the model
var keys = this.keys( this.models );
if( !keys ) return false;
var axis = utils.toArray( this._axis );
// loop through the keys
for( var i in keys ){
// check that the key hasn't been used
if( axis.indexOf( keys[i] ) > -1 ) continue;
// get type of field
if( type == this._findType( keys[i] )) return keys[i];
}
return false;
},
// process an item to match its keys with the schema
_matchAxis: function( item ){
// variables
var chart = this.chart();// get schema from chart
//var axis = this._axis;// existing axis
// prerequisite(s)
var used = [];
var missing = [];
for( var axis in chart.schema ){
if( this._axis[axis] ){
used.push( this._axis[axis] );
continue;
}
missing.push(axis);
}
// exit now if no further axis to process..
if( !missing.length ) return;
// loop through keys
for( var key in item ){
// convention: the first axis is always x (regardless of type)
if( chart.schema.x && !this._axis.x ){
this._axis.x = key;
continue;
}
// don't use the same key twice
if( used.indexOf( key ) > -1 ) continue;
// further processing? (using missing array?)
//this._axis[key] = key;
}
}
}
module.exports = axis;
})()
},{"./utils":17}],7:[function(require,module,exports){
// charts
var force = require('../charts/force'),
pie = require('../charts/pie'),
stack = require('../charts/stack');
var chart = {
// retrieving chart (or adding a new custom chart)
chart: function( chart ){
if (!arguments.length) return this._chart;
// otherwise save provided chart.. evaluate the function first?
// create a new, non-conflicting container if it exists
//if(typeof this.charts[this.options.layout] != "undefined"){
// this.options.layout = "untitled";
//}
var Chart; // class name
// find the right chart
if(typeof chart == "string"){
// find chart from the charts collection
Chart = this.charts[ chart ] || null;
// exit now if we didn't find it...
if( Chart === null ) return this;
} else if(typeof chart == "function"){
Chart = chart;
var layout = chart.prototype.layout || "untitled";
// it it's a new layout...
if(typeof this.charts[ layout ] == "undefined"){
this.charts[this.options.layout] = chart;
// set the new layout automatically?
this.options.layout = layout;
}
}
//
// either way create a new instance of the chart
this._chart = new Chart( this );
//this.charts[this.options.layout]
// preserve chainability
return this;
},
// default layouts
charts: {
force: force,
pie: pie,
stack: stack
},
// loop through charts
//for(var i in charts){
// Cloudvisio.prototype.charts[ charts[i] ] = charts[i];
//}
_lookupSchema: function( type, preferred ){
// fallbacks
type = type || false;
preferred = preferred || false;
// prerequisite
if( !type ) return false;
// variables
var keys = Object.keys( this._axis );
if( !keys ) return false;
var schema = this.chart().schema;
// if there's a preferred field, try to pick that
//console.log("schema", schema);
if( preferred && keys[preferred] && schema[preferred] == type){
console.log("preferred", preferred);
return preferred;
}
for( var i in schema ){
if( schema[i] == type ) return i;
}
return false;
},
// Internal
// - instantiated chart
_chart: null
}
module.exports = chart;
},{"../charts/force":1,"../charts/pie":2,"../charts/stack":3}],8:[function(require,module,exports){
var color = {
// define the color spectrum
colors: function( colors ){
if (!arguments.length) return this.options.colors;
// overwrite existing color palette
this.options.colors = colors;
// preserve chainability
return this;
},
// get the next color in the selected spectrum
color: function(i) {
return (this.options.colors instanceof Function) ? this.options.colors(i) : this.options.colors[i]; // assume it's an array if not a function?
}
}
module.exports = color;
},{}],9:[function(require,module,exports){
var utils = require('./utils');
var data = {
// this is where the axis will be stored
models: [],
// load a dataset
data: function( models, options ){
// fallbacks
options = options || {};
// return the existing data if none is passed...
if ( !arguments.length || models === null ) return ( options.raw ) ? this._data : this._filteredData();
//
var data = this._data;
// check if the data we're adding is in an array
if( models instanceof Array){
// resetting only if set with an option
if( options.reset ){
data = models;
} else {
data = data.concat(models);
}
} else if( models instanceof Object) {
// all individual imports are silent
options.silent = true;
//
//if( options.filter ){
// models.__filter = true;
//}
// passing the key as an option - better way?
if( options.key ){
data[options.key] = models;
} else {
data.push(models);
}
} else {
// nothing else supported for now
return this;
}
// reset the models
if( options.silent ){
// don't erase models
} else {
// reset the models
this.models = [];
}
// save data back to the common container
this._data = data;
// allow method chaining
return this;
},
// add a new item
add: function( item ){
// convert to an array if necessary
item = ( item instanceof Array ) ? item : [item];
// match axis (of first element )
this._matchAxis( item[0] );
// loop through data
for( var i in item ){
// check if there's an id assigned
if( typeof item[i]._id == "undefined" ){
item[i]._id = utils.uniqueID();
}
}
// merge data with existing
this.models = this.models.concat( item );
},
// return filtered data
_filteredData: function(){
var data = [];
for(var i in this._data ){
// if filter is not set yet this is pass-through
if( typeof this._data[i].__filter == "undefined" || this._data[i].__filter ){
data.push( this._data[i] );
}
}
return data;
},
// load a dataset
parse: function( data, options ){
// fallbacks
data = data || false;
options = options || {};
if(!data) return;
// optionally reset
if( options.reset ) this._axis = {};
// check if it's and array of objects
//if(data instanceof Array){
if(data.length != "undefined"){
for( var i in data ){
this.add( data[i] );
}
} else {
// assume one element
this.add( data );
}
// setup the axis by checking if the chart is ready
this.ready();
// allow method chaining
return this;
},
// retrieve all keys from a data block (currently non-recursive)
keys: function( data ){
// fallback to the raw data
data = data || this.data();
//
var keys = [];
for(var i in data){
var attr = Object.keys( data[i] );
for( var j in attr ){
// don't add the same key or queries
if( keys.indexOf( attr[j] ) > -1 || attr[j].search(/__query/gi) > -1 ) continue;
keys.push( attr[j] );
}
}
//
return keys;
},
// set an axis key (or return the whole array
axis: function( key, constant ){
// return the existing data if none is passed...
//if (!arguments.length) return Object.keys( this.models[0] || {} );
if (!arguments.length) return this._axis;
// #24 if key is an object loop through options
if( key instanceof Object ){
for( var k in key ){
this.axis( k, key[k] );
}
}
// focus on a specific subset?
var data = this.data();
// lookup the key in the raw data
for(var i in data){
// create model if necessary
if(typeof this.models[i] == "undefined" ) this.models[i] = { _id: utils.uniqueID() };
// FIX: fallback to value of selected key
var value = data[i][key] || data[i][ this._selectedField ] || false;
if(typeof constant != "undefined"){
// if a constant is passed just use that
this.models[i][key] = constant;
} else if( typeof value == "string" || typeof value == "number"){
// there's currenty a 1-1 match between the data length and the models length...
this.models[i][key] = value;
} else if( typeof value == "boolean" ){
// convert flag to numeric
this.models[i][key] = ( value === true ) ? 1 : 0;
} else if( typeof value == "object" ){
// by priority look up a desciptive key in the object
this.models[i][key] = value.value || value.name || value.title || value.id || value;
} else {
// not supporting any other types for now...
this.models[i][key] = null;
}
//
}
// setup the _axis
// check if this._axis[key] exists?
if( constant ){
this._axis[key] = constant;
} else if( value === data[i][ this._selectedField ] ){
this._axis[key] = this._selectedField;
} else {
this._axis[key] = key;
}
/*
for( var i in obj){
if( i == "chart" ){
var options = this._axis || {};
var chart = obj[i];
// reset options now
this._axis = {};
// don't overwrite existing assignements
for( var j in chart ){
if( options[j] && typeof options[j] != "undefined" && !chart[j] ){
obj[i][j] = options[j];
}
}
}
}
*/
// maintain chanability
return this;
},
// select a field from the raw dataset
select: function( field ){
var keys = this.keys();
this._selectedField = (keys.indexOf(field) > -1 )? field : false;
return this;
},
// calculate axis based on the queries
amount: function( options ){
// fallback
options = options || {};
// get the (active) data
var data = this.data();
var axis = this.axis();
// if there are no value axis (available) in the schema exit now
if( axis.value !== false ) return;
var queries = this.queries();
// also reset models?
if( options.reset ) {
this.models = [];
}
var used = 0;
// lookup all the queries
for(var i in queries ){
// don't use filter queries
if( queries[i].sort == "filter" || queries[i].sort == "exclude" ) continue;
// template
var models = {};
for(var j in data ){
if(typeof data[j][i] == "boolean"){
// skip false state
if( data[j][i] === false ) continue;
// create empty container
if( typeof models[0] == "undefined" ) models[0] = { label: false, value: 0 };
// increment value
models[0].value +=1;
// add label
if( !models[0].label && options.labels !== false ){
models[0].label = queries[i].query.toString();
}
used++;
} else if(typeof data[j][i] == "number"){
// skip false state
if( data[j][i] == -1 ) continue;
// create empty container
if( typeof models[ data[j][i] ] == "undefined" ) models[ data[j][i] ] = { label: false, value: 0 };
// increment value
models[ data[j][i] ].value += 1;
// add label
if( !models[ data[j][i] ].label && options.labels !== false ){
models[ data[j][i] ].label = queries[i].query[ data[j][i] ].toString();
}
used++;
} else {
// what to do with a string?
}
}
// convert object to array
//models = Array.prototype.slice.apply( models );
//this.models = this.models.concat( models );
for( var g in models ){
this.add( models[g] );
}
}
// add another item with the remaining
if( used < data.length ){
this.add({
label: "other",
value: data.length - used
});
}
// lastly specify the axis to the schema
this._axis.label = "label";
this._axis.value = "value";
// allow method chaining
return this;
},
// removing axis
remove: function( string, options ){
// fallbacks
options = options || {};
options.type = options.type || false;
var axis = false,
query = false;
// option types always override any logic
if( !options.type ){
// check in the axis
axis = utils.inArray( string, this._axis );
query = Object.keys(this._queries).indexOf( string );
// an axis is also a query...
if( query > -1 ) options.type = "query";
if( axis > -1 ) options.type = "axis";
}
// if we didn't find anything exit
if( !options.type ) return this;
if( options.type == "query" ){
var q = this._queries[string];
// remove the query entry
delete this._queries[string];
// remove any reference in the data
for( var i in this._data ){
delete this._data[i][string];
// remove the filter flag if query type of filter/exclude or the last query
if( q.sort =="filter" || q.sort =="exclude" || !this._queries.length ){
delete this._data[i].__filter;
}
}
}
if( options.type == "axis" ){
// remove the axis entry
this._axis[string] = false;
// remove the data
for( var j in this.models ){
var model = this.models[j];
if( typeof model[string] != "undefined"){
delete this.models[j][string];
}
}
}
// also support query objects in the future?
return this;
},
// public access of findType
type: function( key, options ){
// lookup the key in the models first
if( this.models.length && this.models[0][key] ){
// everything is fine...
return this._findType( key );
} else {
return this._findType( key, this._data );
}
},
// export axis data in JSON-ready format
toJSON: function(){
// variables
var data = this.models || [];
// cleanup data
for( var i in data ){
delete data[i]._id;
}
// other tweaks?
return data;
},
// Internal
// - raw data container
_data: [],
// - chart fields set as axis
_axis: {},
// - the last selected field
_selectedField: false,
}
module.exports = data;
},{"./utils":17}],10:[function(require,module,exports){
// dependencies
var d3 = require('d3');
var language = {
// convert the regular expression into a string
verbalize: function( query ){
// replace signs
query = query.replace(/\+/gi, " and ");
query = query.replace(/>/gi, " greater than ");
query = query.replace(/</gi, " less than ");
// remove unnecessary spaces
query = query.replace(/ /gi, " ");
},
// Display status messages about the chart generation
status: function( render ){
var flags = {};
var chart = this.chart();
render = render || false;
// check if the data is empty
//if( Object.keys( this._data ).length === 0 ) flags.push("001");
if( this._data.length === 0 ) flags[101] = status[101];
// check if there are any axis
if( this.models.length === 0 ) flags[102] = status[102];
// look into each individual missing axis
for(var i in this._axis ){
var axis = this._axis[i];
if( !axis ){
var type = chart.schema[i];
switch(type){
case "string":
flags[103] = status[103].replace(/_field_/gi, i);
break;
case "number":
flags[104] = status[104].replace(/_field_/gi, i);
break;
}
//console.log(i);
}
}
// if everything is OK return a standard 200
if( Object.keys( flags ).length === 0 ) flags[200] = status[200];
if( render ){
var el = d3.select( this.el );
// create html
var html = '<div class="error">';
html += "<p>There were the following errors:</p>";
html += "<ul>";
for(var j in flags){
html += "<li>"+ flags[j] +"</li>";
}
html += "</ul>";
html += "</div>";
el.html( html );
}
// either way return the flags as an object
return flags;
}
}
// Helpers
var status = {
101: "Missing source data",
102: "No axis created",
103: "Missing string: _field_",
104: "Missing number: _field_",
105: "Missing group: _field_",
200: "OK"
};
module.exports = language;
},{"d3":18}],11:[function(require,module,exports){
var defaults = require('../config/default'),
options = require('./options.js'),
data = require('./data'),
axis = require('./axis'),
queries = require('./queries'),
math = require('./math'),
regex = require('./regex'),
language = require('./language'),
chart = require('./chart'),
//'lib/charts/stack.js',
//'lib/charts/pie.js',
//'lib/charts/force.js',
render = require('./render'),
color = require('./color'),
utils = require('./utils');
Cloudvisio = function( options ){
// variables
options = options || {};
this.options = {};
// get the el
this.el = options.el || defaults.el;
// clean up options
options.layout = ( options.layout || defaults.layout ).toLowerCase();
// extend with the defaults
options = utils.extend( defaults, options );
/*
options.container = options.container || defaults.container;
options.width = options.width || defaults.width;
options.height = options.height || defaults.height;
options.colors = options.colors || defaults.colors;
*/
// add the appropriate chart
//this.chart( options.layout );
// ovewrite the default values (with the passed ones)
this.options = options;
this.set( options ); // why?
// allow method chaining
return this;
};
Cloudvisio.prototype = {
constructor: Cloudvisio,
description : function() {
return "Cloudvisio: Making awesome charts of your data";
},
options: {} // why isn't this available in the constructor?
};
// extend prototype with module methods (temp?)
Cloudvisio.prototype = utils.extend( Cloudvisio.prototype, options, data, axis, queries, math, regex, language, chart, render, color);
module.exports = Cloudvisio;
},{"../config/default":4,"./axis":6,"./chart":7,"./color":8,"./data":9,"./language":10,"./math":12,"./options.js":13,"./queries":14,"./regex":15,"./render":16,"./utils":17}],12:[function(require,module,exports){
(function(){
var math = {
process: function( key, value ) {
console.log(key + " : "+value);
},
eq: function( number, options ){
//
options = options || {};
options.eq = true;
this._filterNumber( number, options );
// allow method chaining
return this;
},
gt: function( number, options ){
//
options = options || {};
options.gt = true;
this._filterNumber( number, options );
// allow method chaining
return this;
},
lt: function( number, options ){
//
options = options || {};
options.lt = true;
this._filterNumber( number, options );
// allow method chaining
return this;
},
// Applying a filter based on an operator
_filterNumber: function( number, options ){
//
options = options || {};
var field = this._selectedField || options.field || false;
// exit now if theres no selected field
if( !field ) return;
// check if the query is a number
if(isNaN(number)) return;
// in case of a string
number = parseFloat(number);
// get the data
var data = this.data(null, { raw : true });
// create a new query
var type = null;
if( options.eq) type = "eq";
if( options.lt) type = "lt";
if( options.gt) type = "gt";
//
var id = this.queries({ field: field, type: type, query: number }, options);
//
for( var i in data ){
var opt = {
silent: true,
filter: options.filter
};
var result;
if( options.eq){
result = (data[i][field] === number);
}
if( options.lt){
result = (data[i][field] < number);
}
if( options.gt){
result = (data[i][field] > number);
}
// make the necessary adjustments to the data
if( options.exclude && data[i].__filter !== false ){
// when exluding don't consider the ones already filtered out
data[i].__filter = !result;
} else if( options.filter && data[i].__filter !== false ){
data[i].__filter = result;
}
// add a new query key
data[i][id] = result;
//
opt.key = i;
// update the existing data
this.data( data[i], opt);
}
}
}
module.exports = math;
})()
},{}],13:[function(require,module,exports){
var utils = require('./utils');
var options = {
// updating options dynamically
set: function( obj ){
if( !(obj instanceof Object) ) return this;
// merge the final object
utils.extend(this.options, obj);
// special condition for layouts
if( obj.layout ) this.chart( obj.layout );
//
return this;
},
// retrieving options
get: function( key ){
//
return this.options[key] || false;
}
}
module.exports = options;
},{"./utils":17}],14:[function(require,module,exports){
var utils = require('./utils');
var queries = {
// set or retrieve the queries applied
queries: function( query, options ){
if (!arguments.length) return this._queries;
// fallbacks
query = query || false;
options = options || {};
options.restore = options.restore || false;
// exit now if there; sno query
if( !query ) return this;
// get a query if given string
if(typeof query == "string") return this._queries[ query ];
// check if the query passed is a set of existing queries
if(!options.restore && query instanceof Object){
for( var q in query ){
if( q.search(/__query/gi) > -1 ){
// restoring previously set queries
options.restore = true;
}
}
}
// set a query if given object
var queries = (options.restore || query instanceof Array) ? query : [query];
//
var id = false;
for( var i in queries ){
// check if query exists first?
if( options.restore ){
id = i;
}
// create a new field for the query
id = (!id && queries[i].id) ? queries[i].id : this._queryId();
// get the sort if not set (this could also be done in _filterString, _filterNumber)
if( typeof queries[i].sort == "undefined"){
//
if( options.exclude ){
queries[i].sort = "exclude";
}else if( options.filter ){
queries[i].sort = "filter";
} else {
queries[i].sort = "group";
}
}
// insert selected data (field, type, query )
this._queries[id] = { field: queries[i].field, type: queries[i].type, query: queries[i].query, sort: queries[i].sort };
}
// return the id if entering one query
return (query instanceof Array) ? this : id;
},
// refresh results of queries
refresh: function(){
// variables
var queries = this.queries;
// discart filter?
// loop through queries
for(var i in queries){
//
this.queries(queries[i]);
}
},
// Internal
// - saving queries
_queries: {},
_queryId: function(){
var prefix = "__query_";
var queries = Object.keys( this._queries );
var id;
do {
id = utils.uid();
} while ( queries.indexOf( prefix + id ) > -1 );
return prefix + id;
}
}
module.exports = queries;
},{"./utils":17}],15:[function(require,module,exports){
(function(){var utils = require('./utils');
var regex = {
// Public methods
match: function( string, options ){
//
options = options || {};
options.match = true;
this._filterString( string, options );
// allow method chaining
return this;
},
// filter the data based on a regular expression
search: function( string, options ){
//
options = options || {};
options.search = true;
this._filterString( string, options );
// allow method chaining
return this;
},
// lookup a value in a field
find: function( query, options ){
//
options = options || {};
if( typeof query == "boolean" ) {
this.match( query, options );
} else {
// a find() always resets the axis?
this.search( query, options );
}
// allow method chaining
return this;
/*
options = options || {};
// exit now if theres no selected field
if( !this._selectedField ) return;
var field = this._selectedField;
// check if the query is a boolean
if( typeof query == "boolean" ) query = (query) ? "true" : "false";
// a find() always resets the axis?
options.reset = true;
this.group( [query], field, options);
// allow method chaining
return this;
*/
},
// create an axis by grouping the raw data
group: function(){
// for no arguments (or more arguments) just exit
//if( !arguments.length ) return this;
// variables
var field, groups, options;
// count arguments (groups are optional)
switch( arguments.length ){
case 1:
if( arguments[0] instanceof Array ){
groups = arguments[0];
} else {
options = arguments[0];
}
break;
default:
groups = arguments[0];
options = arguments[1];
break;
}
// fallbacks
options = options || {};
field = this._selectedField || options.field || false;
// get the (working) data
var data = this.data();
//
// try to find an existing query first?
//
if( groups ){
// convert groups to lower case
groups = groups.join("`").toLowerCase().split("`");
options.fixed = true; // fixed groups
} else {
groups = this._getValues( field, data );
}
// save the query
var id = this.queries({ field: field, type: "group", query: groups }, options);
//
for( var i in data ){
// convert any value to string
var value = (data[i][field] instanceof Object) ? utils.toArray( data[i][field] ).join(",") : ""+data[i][field]+"";
var opt = {
silent: true,
key: i
};
var result = this._find( value, groups );
// add a new query key
// are we expecting more than one matches??
//if(result instanceof Array) result = result.pop().toLowerCase();
data[i][id] = result;
// update the existing data
this.data( data[i], opt);
}
/*
// reset models if needed
if( options.reset ){
// create a model set equal to the length of the groups
var length = (groups.length === 1) ? groups.length+1 : groups.length;
this.models = new Array( length );
field = "value";
for( var k = 0; k < this.models.length; k++ ){
this.models[k] = {};
this.models[k][field] = 0;
if( options.labels ){
this.models[k].label = groups[k] || "not "+ groups[k-1];
}else {
this.models[k].label = "";
}
}
} else {
field = "group_"+key;
}
// lookup the right axis
for( var i in data ){
var model;
if( !options.reset ){
// create the model if this is the first axis
this.models[i] = this.models[i] || {};
model = this.models[i];
}
if( typeof data[i][key] != "undefined"){
// convert any value to string
var value = (data[i][key] instanceof Object) ? utils.toArray( data[i][key] ).join("|") : ""+data[i][key]+"";
//
var matches = this._find(groups, value);
// process results
//if(matches !== null);
if(matches instanceof Array){
// are we expecting more than one matches??
var group = matches.pop().toLowerCase();
if( options.reset ){
this.models[ groups.indexOf( group ) ][field] += 1;
} else {
this.models[i][field] = groups.indexOf( group );
}
} else {
if( options.reset ){
this.models[groups.length][field] += 1;
} else {
this.models[i][field] = -1;
}
}
/*
// support types other than string
switch(typeof value){
case "string":
break;
case "boolean":
console.log(groups);
// in this case we do a direct comparison (sting comparisson
this.models[i]["group_"+key] = groups.indexOf( value.toString() );
console.log();
break;
}
*/
/*
} else {
//model["group_"+axis] = null;
if( options.reset ){
this.models[groups.length][field] += 1;
} else {
this.models[i][field] = -1;
}
}
}
// save latest group
// #30 pick the right axis
var selected = this._lookupSchema("number", field);
this._axis[selected] = field;
*/
return this;
},
reverse: function(){
// set the flag for reference
this._reverseQuery = true;
// allow method chaining
return this;
},
// check if a query is there
inQueries: function( q ){
var id = false;
for( var i in this._queries ){
// check all fields for a match
if( this._queries[i].field == q.field && this._queries[i].type == q.type && this._queries[i].query == q.query ){
id = i;
}
}
return id;
},
// Internal objects
_reverseQuery: false,
// Internal methods
// Return the matches of a regular expression
_find: function( query, string ){
// convert text to lower case
string = string.join("|").toLowerCase();
query = query.toLowerCase();
//
var regexp = new RegExp(string, "gi");
var matches = query.match(regexp);
if(matches === null) return -1;
// collapse matches / explode query
matches = matches.join(",");
string = string.split("|");
if( string instanceof Array){
// return the index
return string.indexOf( matches );
}
// return the text
return matches;
},
// get a list of the different values
_getValues: function( key, data ){
var values = [];
for( var i in data ){
// convert any value to string
var value = (data[i][key] instanceof Object) ? utils.toArray( data[i][key] ).join(",") : ""+data[i][key]+"";
var exists = values.indexOf( value ) > -1;
if( !exists ) values.push(value);
}
return values;
},
// Applying a filter based on an regulat expression
_filterString: function( string, options ){
//
options = options || {};
var field = this._selectedField || options.field || false;
// exit now if theres no selected field
if( !field ) return;
// check if the query is a number
//if(typeof string != "string") return;
// get the data
var data = this.data(null, { raw : true });
//
var type;
if( options.match ) type = "match";
if( options.search ) type = "search";
// create a new query
var id = this.queries({ field: field, type: type, query: string }, options);
//
for( var i in data ){
var opt = {
silent: true,
filter: options.filter
};
var result;
if( options.match ){
result = (data[i][field] === string);
}
if( options.search ){
var exp = new RegExp(string, "gi");
result = ( data[i][field].search(exp) > -1 );
}
// make the necessary adjustments to the data
if( options.exclude && data[i].__filter !== false ){
// when exluding don't consider the ones already filtered out
data[i].__filter = !result;
} else if( options.filter && data[i].__filter !== false ){
data[i].__filter = result;
}
// add a new query key
data[i][id] = result;
//
opt.key = i;
//opt.filter = true;
// update the existing data
this.data( data[i], opt);
}
}
}
module.exports = regex;
})()
},{"./utils":17}],16:[function(require,module,exports){
(function(){var utils = require('./utils');
var render = {
// rendering the visualization (generated once)
render: function( append ){
// fallbacks
append = append || false;
// chart is a prerequisite
var chart = this.chart();
if( chart === null) return;
var options = {
append : append
};
if( !this.ready() ){
// exit now with an error message
return ( this.options.renderErrors ) ? this.status("string") : this.status();
}
// ready to create chart
// clear container (by default)
if( !append ) {
this._container();
}
chart.render( options );
return this.status();
//.transition().duration(500).call( this.chart() );
},
// updating visualization (on every tick)
update: function(){
},
// verifies that there's the minimum data for a chart
ready: function( layout ){
// fallback (not needed?)
//layout = layout || this.options.layout;
// check if the selected layout "needs" a group
this._findGroups();
// process query amounts
this.amount();
// prerequisite - we need to have some data set...
if( this.models.length === 0 ) return false;
// main flag
var state = true,
chart, original_chart, original_options;
//
if(!arguments.length){
// evaluate the current layout
chart = this.chart();
} else {
// keep a reference of the original chart / options
original_chart = this.chart();
original_options = this._axis;
this.chart( layout );
chart = this.chart();
}
// check if all the required attributes are set
for( var i in chart.schema ){
var option = this._axis[i] || false;
if(!option){
// lookup a suitable option
var type = chart.schema[i];
option = this._findAxis( type );
if( option ) {
this._axis[i] = option;
continue;
} else {
state = false;
}
}
}
// restore original layout if necessery
if( arguments.length ){
this._chart = original_chart;
this._axis = original_options;
}
//
return state;
},
// reset all properties/data
reset: function(){
// variables
var data, options;
// count arguments
switch( arguments.length ){
case 1:
if( arguments[0] instanceof Array ){
data = arguments[0];
} else {
options = arguments[0];
}
break;
default:
data = arguments[0];
options = arguments[1];
break;
}
// fallbacks
data = data || false;
options = options || false;
if( data ) this.data( data, { reset: true });
// optionally reset other options
if( !options.soft || options.hard ){
this.options = defaults;
this.models = [];
}
},
// Internal methods
// creates the chart container
_container: function(){
var width = 4000,
height = 3000;
var svg = d3.select( this.el ).html("")
.append( this.options.container );
function redraw() {
svg.attr("transform", "translate("+ d3.event.translate +")" + " scale("+ d3.event.scale +")");
}
svg.attr({
"width": this.options.width,
"height": this.options.height
})
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("pointer-events", "all")
.call(d3.behavior.zoom().on("zoom", redraw));
return svg;
},
// get the type of the selected field
_findType: function( key, data ){
var type = false;
data = data || this.models;
// loop through models
for(var i in data){
var value = data[i][key];
// #35 check if the value is a float
if( !isNaN( parseFloat(value) ) ) value = parseFloat(value);
var itype = (typeof value);
// if we've found more than one types exit
if( type && type != itype ) return "mixed";
type = itype;
}
return type;
},
// get the type of the selected field
_findGroups: function( key, data ){
var axis = this.axis();
var queries = this.queries();
// first see if we have a group "need"
if( axis.group === false ){
var match = false;
for( var i in queries ){
if(queries[i].type == "group"){
// select the query key as the group axis
this.axis(i);
this._axis.group = i;
// one group is enough?
match = true;
break;
}
}
// fallback to unique id
if(!match) this._axis.group = "_id";
}
}
}
module.exports = render;
})()
},{"./utils":17}],17:[function(require,module,exports){
(function(){
var utils = {
// Common.js extend method: https://github.com/commons/common.js
extend: function(){
var objects = Array.prototype.slice.call( arguments ); // to array?
var destination = {};
for( var obj in objects ){
var source = objects[obj];
for (var property in source){
if (source[property] && source[property].constructor && source[property].constructor === Object) {
destination[property] = destination[property] || {};
destination[property] = arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
}
return destination;
},
// convert an object to an array (loosing the keys)
toArray: function( obj ){
var array = [];
for( var i in obj ){
array.push( obj[i] );
}
return array;
},
// checks if an object is in an array
inArray: function( obj, list ){
for (var i = 0; i < list.length; i++) {
if (list[i] === obj) {
return i;
}
}
return -1;
},
// unique sequential id - based on: http://stackoverflow.com/a/14714979
uid: (function(){var id=0;return function(){if(arguments[0]===0)id=0;return id++;};})(),
// remove all special characters & spaces
safeString: function( string ){
return string.replace(/[^a-zA-Z0-9]/g,'');
},
// - Creates a unique id for identification purposes
uniqueID : function (separator) {
var delim = separator || "-";
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return (S4() + S4() + delim + S4() + delim + S4() + delim + S4() + delim + S4() + S4() + S4());
},
// - load template
loadTemplate: function(file){
return file;
},
// Usage:
//traverse(options.data,process);
traverse: function(o,func){
for ( var i in o) {
func.apply(this, [i, o[i]]);
if (typeof o[i] == "object" ) {
//going on step down in the object tree!!
traverse(o[i], func);
}
}
}
};
module.exports = utils;
})()
},{}],18:[function(require,module,exports){
!fu