diff --git a/src/kibana/apps/visualize/controllers/visualize.js b/src/kibana/apps/visualize/controllers/visualize.js
new file mode 100644
index 0000000000000..70588d921fdae
--- /dev/null
+++ b/src/kibana/apps/visualize/controllers/visualize.js
@@ -0,0 +1,140 @@
+define(function (require) {
+ var _ = require('lodash');
+ var app = require('modules').get('app/visualize');
+
+ require('../factories/vis');
+ require('../services/aggs');
+
+ app.controller('Visualize', function ($scope, courier, createNotifier, Vis, Aggs) {
+ var notify = createNotifier({
+ location: 'Visualize Controller'
+ });
+
+ // the object detailing the visualization
+ var vis = $scope.vis = window.vis = new Vis({
+ metric: {
+ label: 'Y-Axis',
+ min: 1,
+ max: 1
+ },
+ segment: {
+ label: 'X-Axis',
+ min: 1,
+ max: 1
+ },
+ group: {
+ label: 'Color',
+ max: 10
+ },
+ split: {
+ label: 'Rows & Columns',
+ max: 2
+ }
+ }, {
+ split: [
+ {
+ field: 'response',
+ size: 5,
+ agg: 'terms'
+ },
+ {
+ field: '_type',
+ size: 5,
+ agg: 'terms'
+ }
+ ],
+ segment: [
+ {
+ field: '@timestamp',
+ interval: 'week'
+ }
+ ],
+ group: [
+ {
+ field: 'extension',
+ size: 5,
+ agg: 'terms',
+ global: true
+ }
+ ]
+ });
+
+ vis.dataSource.$scope($scope);
+
+ $scope.refreshFields = function () {
+ $scope.fields = null;
+ vis.dataSource.clearFieldCache().then(getFields, notify.error);
+ };
+
+ function getFields() {
+ vis.dataSource.getFields(function (err, fieldsHash) {
+ if (err) return notify.error(err);
+
+ // create a sorted list of the fields for display purposes
+ $scope.fields = _(fieldsHash)
+ .keys()
+ .sort()
+ .transform(function (fields, name) {
+ var field = fieldsHash[name];
+ field.name = name;
+ fields.push(field);
+ })
+ .value();
+
+ $scope.fields.byName = fieldsHash;
+ });
+ }
+ // get the fields for initial display
+ getFields();
+
+ $scope.Vis = Vis;
+ $scope.Aggs = Aggs;
+
+ $scope.updateDataSource = function () {
+ notify.event('update data source');
+ var config = _.groupBy(vis.getConfig(), function (config) {
+ switch (config.categoryName) {
+ case 'group':
+ case 'segment':
+ return 'dimension';
+ default:
+ return config.categoryName;
+ }
+ });
+
+ if (!config.dimension) {
+ // use the global aggregation if we don't have any dimensions
+ config.dimension = [{
+ agg: 'global'
+ }];
+ }
+
+ var dsl = {};
+ var i = 0;
+
+ var nest = (function () {
+ var current = dsl;
+ return function (config) {
+ current.aggs = {};
+ var key = '_agg_' + (i++);
+
+ var aggDsl = {};
+ aggDsl[config.agg] = config.aggParams;
+
+ current = current.aggs[key] = aggDsl;
+ };
+ }());
+
+ config.split && config.split.forEach(nest);
+ config.dimension && config.dimension.forEach(nest);
+ config.metric && config.metric.forEach(nest);
+
+ notify.log('config', config);
+ notify.log('aggs', dsl.aggs);
+
+ vis.dataSource.aggs(dsl.aggs).fetch();
+ notify.event('update data source', true);
+ };
+
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/directives/canvas.js b/src/kibana/apps/visualize/directives/canvas.js
new file mode 100644
index 0000000000000..dce77da9fe2c8
--- /dev/null
+++ b/src/kibana/apps/visualize/directives/canvas.js
@@ -0,0 +1,25 @@
+define(function (require) {
+ var module = require('modules').get('app/visualize');
+ var $ = require('jquery');
+
+ module.directive('visCanvas', function () {
+ return {
+ restrict: 'A',
+ link: function ($scope, $el) {
+ var $window = $(window);
+ var $header = $('.content > nav.navbar:first()');
+
+ function stretchVis() {
+ $el.css('height', $window.height() - $header.height() - 30);
+ }
+
+ stretchVis();
+
+ $window.on('resize', stretchVis);
+ $scope.$on('$destroy', function () {
+ $window.off('resize', stretchVis);
+ });
+ }
+ };
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/directives/config_category.js b/src/kibana/apps/visualize/directives/config_category.js
new file mode 100644
index 0000000000000..ace93cf7ccc55
--- /dev/null
+++ b/src/kibana/apps/visualize/directives/config_category.js
@@ -0,0 +1,22 @@
+define(function (require) {
+ var html = require('text!../partials/config_category.html');
+
+ require('./config_editor');
+
+ require('modules')
+ .get('app/visualize')
+ .directive('visConfigCategory', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ categoryName: '=',
+ vis: '=',
+ fields: '='
+ },
+ template: html,
+ link: function ($scope, $el) {
+ $scope.category = $scope.vis[$scope.categoryName];
+ }
+ };
+ });
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/directives/config_controlls.js b/src/kibana/apps/visualize/directives/config_controlls.js
new file mode 100644
index 0000000000000..88458190af110
--- /dev/null
+++ b/src/kibana/apps/visualize/directives/config_controlls.js
@@ -0,0 +1,58 @@
+define(function (require) {
+ var app = require('modules').get('app/visualize');
+ var _ = require('lodash');
+
+ var templates = {
+ orderAndSize: require('text!../partials/controls/order_and_size.html'),
+ interval: require('text!../partials/controls/interval.html'),
+ globalLocal: require('text!../partials/controls/global_local.html')
+ };
+
+ app.directive('visConfigControls', function ($compile, Vis, Aggs) {
+ return {
+ restrict: 'E',
+ scope: {
+ config: '='
+ },
+ link: function ($scope, $el, attr) {
+ var $controls = $el.find('.agg-param-controls');
+
+ $scope.$watch('config.agg', function (aggName) {
+ var agg = Aggs.aggsByName[aggName];
+ var controlsHtml = '';
+
+ if (agg) {
+ var aggParams = $scope.aggParams = agg.params;
+
+ _.forOwn(aggParams, function (param, name) {
+ // if the param doesn't have options, or a default value, skip it
+ if (!param.options) return;
+ // if there isn't currently a value, or the current value is not one of the options, set it to the default
+ if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
+ $scope.config[name] = param.default;
+ }
+ });
+
+ if (aggParams.order && aggParams.size) {
+ controlsHtml += ' ' + templates.orderAndSize;
+ }
+
+ if (aggParams.interval) {
+ controlsHtml += ' ' + templates.interval;
+ }
+
+ if ($scope.config.categoryName === 'group') {
+ controlsHtml += ' ' + templates.globalLocal;
+ }
+ }
+
+ $controls.html($compile(controlsHtml)($scope));
+ });
+
+ $scope.Aggs = Aggs;
+ $scope.Vis = Vis;
+ }
+ };
+ });
+
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/directives/config_editor.js b/src/kibana/apps/visualize/directives/config_editor.js
new file mode 100644
index 0000000000000..fc37e097f1e1f
--- /dev/null
+++ b/src/kibana/apps/visualize/directives/config_editor.js
@@ -0,0 +1,133 @@
+define(function (require) {
+ var app = require('modules').get('app/visualize');
+ var _ = require('lodash');
+ var $ = require('jquery');
+
+ require('filters/field_type');
+
+ app.directive('visConfigEditor', function ($compile, Vis, Aggs) {
+ var categoryOptions = {
+ metric: {
+ template: require('text!../partials/editor/metric.html')
+ },
+ segment: {
+ template: require('text!../partials/editor/dimension.html'),
+ setup: setupDimension
+ },
+ group: {
+ template: require('text!../partials/editor/dimension.html'),
+ setup: setupDimension
+ },
+ split: {
+ template: require('text!../partials/editor/dimension.html'),
+ setup: setupDimension
+ }
+ };
+
+ var controlTemplates = {
+ orderAndSize: require('text!../partials/controls/order_and_size.html'),
+ interval: require('text!../partials/controls/interval.html'),
+ globalLocal: require('text!../partials/controls/global_local.html')
+ };
+
+ // generalized setup for group and segment
+ function setupDimension($scope, $el) {
+ var $controls = $el.find('.agg-param-controls');
+
+ function getAvailableAggsForField() {
+ if (!$scope.config.field || !$scope.fields) return;
+
+ var field = $scope.fields.byName[$scope.config.field];
+
+ // clear the previous choices
+ $scope.availableAggs = void 0;
+ // get the new choices
+ var aggs = Aggs.aggsByFieldType[field.type];
+
+ if (!aggs || aggs.length === 0) {
+ // init or invalid field type
+ $scope.config.agg = void 0;
+ return;
+ }
+
+ if (aggs.length === 1) {
+ // only once choice, make it for the user
+ $scope.config.agg = aggs[0].name;
+ return;
+ }
+
+ // set the new choices
+ $scope.availableAggs = aggs;
+
+ // update the agg only if it is not currently a valid option
+ if (!$scope.config.agg || !_.find(aggs, { name: $scope.config.agg })) {
+ $scope.config.agg = aggs[0].name;
+ return;
+ }
+ }
+
+ // since this depends on the field and field list, watch both
+ $scope.$watch('config.field', getAvailableAggsForField);
+ $scope.$watch('fields', getAvailableAggsForField);
+
+ $scope.$watch('config.agg', function (aggName) {
+ var agg = Aggs.aggsByName[aggName];
+ var controlsHtml = '';
+
+ if (agg) {
+ var params = $scope.aggParams = agg.params;
+
+ _.forOwn(params, function (param, name) {
+ // if the param doesn't have options, or a default value, skip it
+ if (!param.options) return;
+ // if there isn't currently a value, or the current value is not one of the options, set it to the default
+ if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
+ $scope.config[name] = param.default;
+ }
+ });
+
+ if (params.order && params.size) {
+ controlsHtml += ' ' + controlTemplates.orderAndSize;
+ }
+
+ if (params.interval) {
+ controlsHtml += ' ' + controlTemplates.interval;
+ if (!controlsHtml.match(/aggParams\.interval\.options/)) debugger;
+ }
+
+ if ($scope.config.categoryName === 'group') {
+ controlsHtml += ' ' + controlTemplates.globalLocal;
+ }
+ }
+
+ $controls.html($compile(controlsHtml)($scope));
+ });
+ }
+
+ return {
+ restrict: 'E',
+ scope: {
+ config: '=',
+ fields: '=',
+ vis: '='
+ },
+ link: function ($scope, $el, attr) {
+ var categoryName = $scope.config.categoryName;
+ var opts = categoryOptions[categoryName];
+
+ $scope.Aggs = Aggs;
+ $scope.Vis = Vis;
+
+ // attach a copy of the template to the scope and render
+ $el.html($compile(opts.template)($scope));
+
+ _.defaults($scope.val, opts.defVal || {});
+ if (opts.setup) opts.setup($scope, $el);
+
+ // rather than accessing vis.{{categoryName}} everywhere
+ $scope[categoryName] = $scope.vis[categoryName];
+ }
+ };
+ });
+
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/directives/visualization.js b/src/kibana/apps/visualize/directives/visualization.js
new file mode 100644
index 0000000000000..b080eb5ee2a4b
--- /dev/null
+++ b/src/kibana/apps/visualize/directives/visualization.js
@@ -0,0 +1,31 @@
+define(function (require) {
+ var converters = require('../resp_converters/index');
+ // var K4D3 = require('K4D3');
+
+ function VisualizationDirective() {
+ return {
+ restrict: 'E',
+ template: '
{{ results | json }}
',
+ scope: {
+ vis: '='
+ },
+ link: function ($scope, $el) {
+ var vis = $scope.vis;
+
+ vis
+ .dataSource
+ .on('results', function (resp) {
+ $scope.results = vis.buildChartDataFromResponse(resp).groups;
+ });
+
+ if (!vis.dataSource._$scope) {
+ // only link if the dataSource isn't already linked
+ vis.dataSource.$scope($scope);
+ }
+ }
+ };
+ }
+
+ require('modules').get('kibana/directive')
+ .directive('visualization', VisualizationDirective);
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/factories/vis.js b/src/kibana/apps/visualize/factories/vis.js
new file mode 100644
index 0000000000000..3bc79127ce1e3
--- /dev/null
+++ b/src/kibana/apps/visualize/factories/vis.js
@@ -0,0 +1,306 @@
+define(function (require) {
+ var converters = require('../resp_converters/index');
+ var _ = require('lodash');
+
+ require('../services/aggs');
+
+ function VisFactory(Aggs, $rootScope, $q, createNotifier) {
+ var notify = createNotifier({
+ location: 'Visualization'
+ });
+
+ function Vis(config, state) {
+ config = config || {};
+
+ // the visualization type
+ this.type = config.type || 'histogram';
+
+ // the dataSource that will populate the
+ this.dataSource = $rootScope.rootDataSource.extend().size(0);
+
+ // master list of configs, addConfig() writes here and to the list within each
+ // config category, removeConfig() does the inverse
+ this.configs = [];
+
+ // setup each config category
+ Vis.configCategories.forEach(function (category) {
+ var myCat = _.defaults(config[category.name] || {}, category.defaults);
+ myCat.configs = [];
+ this[category.name] = myCat;
+ }, this);
+
+ if (state) {
+ // restore the passed in state
+ this.setState(state);
+ } else {
+ this._fillConfigsToMinimum();
+ }
+ }
+
+ Vis.configCategories = [
+ {
+ name: 'segment',
+ defaults: {
+ min: 0,
+ max: Infinity
+ },
+ configDefaults: {
+ size: 5
+ }
+ },
+ {
+ name: 'metric',
+ defaults: {
+ min: 0,
+ max: 1
+ },
+ configDefaults: {
+ agg: 'count'
+ }
+ },
+ {
+ name: 'group',
+ defaults: {
+ min: 0,
+ max: 1
+ },
+ configDefaults: {
+ global: true,
+ size: 5
+ }
+ },
+ {
+ name: 'split',
+ defaults: {
+ min: 0,
+ max: 2
+ },
+ configDefaults: {
+ size: 5
+ }
+ }
+ ];
+ Vis.configCategoriesByName = _.indexBy(Vis.configCategories, 'name');
+
+ Vis.prototype.addConfig = function (categoryName) {
+ var category = Vis.configCategoriesByName[categoryName];
+ var config = _.defaults({}, category.configDefaults);
+ config.categoryName = category.name;
+
+ this.configs.push(config);
+ this[category.name].configs.push(config);
+
+ return config;
+ };
+
+ Vis.prototype.removeConfig = function (config) {
+ if (!config) return;
+
+ _.pull(this.configs, config);
+ _.pull(this[config.categoryName].configs, config);
+ };
+
+ Vis.prototype.setState = function (state) {
+ var vis = this;
+
+ vis.dataSource.getFields(function (fields) {
+ vis.configs = [];
+
+ _.each(state, function (categoryStates, configCategoryName) {
+ if (!vis[configCategoryName]) return;
+
+ vis[configCategoryName].configs = [];
+
+ categoryStates.forEach(function (configState) {
+ var config = vis.addConfig(configCategoryName);
+ _.assign(config, configState);
+ });
+ });
+
+ vis._fillConfigsToMinimum();
+ });
+ };
+
+ Vis.prototype._fillConfigsToMinimum = function () {
+ var vis = this;
+
+ // satify the min count for each category
+ Vis.configCategories.forEach(function (category) {
+ var myCat = vis[category.name];
+ if (myCat.configs.length < myCat.min) {
+ _.times(myCat.min - myCat.configs.length, function () {
+ vis.addConfig(category.name);
+ });
+ }
+ });
+ };
+
+ /**
+ * Create a list of config objects, which are ready to be turned into aggregations,
+ * in the order which they should be executed.
+ *
+ * @return {Array} - The list of config objects
+ */
+ Vis.prototype.getConfig = function () {
+ var cats = {
+ split: [],
+ global: [],
+ segment: [],
+ local: [],
+ metric: []
+ };
+
+ this.configs.forEach(function (config) {
+ var pos = config.categoryName;
+ if (pos === 'group') pos = config.global ? 'global' : 'local';
+
+ if (!config.field || !config.agg) return;
+
+ var agg = Aggs.aggsByName[config.agg];
+ if (!agg || agg.name === 'count') return;
+
+ var params = {
+ categoryName: config.categoryName,
+ agg: config.agg,
+ aggParams: {
+ field: config.field
+ }
+ };
+
+ // ensure that all of the declared params for the agg are declared on the config
+ var valid = _.every(agg.params, function (paramDef, name) {
+ if (!config[name]) return;
+ if (!paramDef.custom && paramDef.options && !_.find(paramDef.options, { val: config[name] })) return;
+
+ // copy over the param
+ params.aggParams[name] = config[name];
+
+ // allow provide a hook to covert string values into more complex structures
+ if (paramDef.toJSON) {
+ params.aggParams[name] = paramDef.toJSON(params.aggParams[name]);
+ }
+
+ return true;
+ });
+
+ if (valid) cats[pos].push(params);
+ });
+
+ return cats.split.concat(cats.global, cats.segment, cats.local, cats.metric);
+ };
+
+ /**
+ * Transform an ES Response into data for this visualization
+ * @param {object} resp The elasticsearch response
+ * @return {array} An array of flattened response rows
+ */
+ Vis.prototype.buildChartDataFromResponse = function (resp) {
+ notify.event('convert ES response');
+
+ function createGroup(bucket) {
+ var g = {};
+ if (bucket) g.key = bucket.key;
+ return g;
+ }
+
+ function finishRow(bucket) {
+ // collect the count and bail, free metric!!
+ level.rows.push(row.concat(bucket.value === void 0 ? bucket.doc_count : bucket.value));
+ }
+
+ // all aggregations will be prefixed with:
+ var aggKeyPrefix = '_agg_';
+ var converter = converters[this.type];
+
+ // as we move into the different aggs, shift configs
+ var childConfigs = this.getConfig();
+ var lastCol = childConfigs[childConfigs.length - 1];
+
+ // into stack, and then back when we leave a level
+ var stack = [];
+ var row = [];
+
+ var chartData = createGroup();
+ var level = chartData;
+
+ (function splitAndFlatten(bucket) {
+ var col = childConfigs.shift();
+ // add it to the top of the stack
+ stack.unshift(col);
+
+ _.forOwn(bucket, function (result, key) {
+ // filter out the non prefixed keys
+ if (key.substr(0, aggKeyPrefix.length) !== aggKeyPrefix) return;
+
+ if (col.categoryName === 'split') {
+ var parent = level;
+ result.buckets.forEach(function (bucket) {
+ var group = createGroup(bucket);
+
+ if (parent.groups) parent.groups.push(group);
+ else parent.groups = [group];
+
+ level = group;
+ splitAndFlatten(bucket);
+ if (group.rows && group.columns) {
+ group.data = converter(group.columns, group.rows);
+ delete group.rows;
+ delete group.columns;
+ }
+ });
+
+ level = parent;
+ return;
+ }
+
+ if (!level.columns || !level.rows) {
+ // setup this level to receive records
+ level.columns = [stack[0]].concat(childConfigs);
+ level.rows = [];
+
+ // the columns might now end in a metric, but the rows will
+ if (childConfigs[childConfigs.length - 1].categoryName !== 'metric') {
+ level.columns.push({
+ categoryName: 'metric',
+ agg: 'count'
+ });
+ }
+ }
+
+ if (col.categoryName === 'metric') {
+ // one row per bucket
+ finishRow(result);
+ } else {
+ // keep digging
+ result.buckets.forEach(function (bucket) {
+ // track this bucket's "value" in our temporary row
+ row.push(bucket.key);
+
+ if (col === lastCol) {
+ // also grab the bucket's count
+ finishRow(bucket);
+ } else {
+ splitAndFlatten(bucket);
+ }
+
+ row.pop();
+ });
+ }
+ });
+
+ childConfigs.unshift(stack.shift());
+ })(resp.aggregations);
+
+ notify.event('convert ES response', true);
+
+
+ return chartData;
+ };
+
+ return Vis;
+ }
+
+ require('modules')
+ .get('kibana/services')
+ .factory('Vis', VisFactory);
+});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/index.html b/src/kibana/apps/visualize/index.html
index 9f5d89e1c5b6f..b2a129a21220e 100644
--- a/src/kibana/apps/visualize/index.html
+++ b/src/kibana/apps/visualize/index.html
@@ -1 +1,22 @@
-
Visualize
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/index.js b/src/kibana/apps/visualize/index.js
index 4de8f308fc741..0bd9e161e3f1c 100644
--- a/src/kibana/apps/visualize/index.js
+++ b/src/kibana/apps/visualize/index.js
@@ -1,5 +1,9 @@
define(function (require) {
require('css!./styles/main.css');
- var app = require('modules').get('app/visualize');
+ require('./controllers/visualize');
+
+ require('./directives/config_category');
+ require('./directives/canvas');
+ require('./directives/visualization');
});
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/config_category.html b/src/kibana/apps/visualize/partials/config_category.html
new file mode 100644
index 0000000000000..e441eece215da
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/config_category.html
@@ -0,0 +1,22 @@
+
+
+ {{ category.label }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/controls/global_local.html b/src/kibana/apps/visualize/partials/controls/global_local.html
new file mode 100644
index 0000000000000..eb18d744a0333
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/controls/global_local.html
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/controls/interval.html b/src/kibana/apps/visualize/partials/controls/interval.html
new file mode 100644
index 0000000000000..d40275818d1f3
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/controls/interval.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/controls/order_and_size.html b/src/kibana/apps/visualize/partials/controls/order_and_size.html
new file mode 100644
index 0000000000000..71d5e7c16d215
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/controls/order_and_size.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/editor/dimension.html b/src/kibana/apps/visualize/partials/editor/dimension.html
new file mode 100644
index 0000000000000..edff5030c8332
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/editor/dimension.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/kibana/apps/visualize/partials/editor/metric.html b/src/kibana/apps/visualize/partials/editor/metric.html
new file mode 100644
index 0000000000000..e616a88837b4e
--- /dev/null
+++ b/src/kibana/apps/visualize/partials/editor/metric.html
@@ -0,0 +1,21 @@
+