diff --git a/package.json b/package.json
index fbca397f7acffa..ee269cb8144d15 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
"commander": "^2.8.1",
"css-loader": "^0.15.1",
"d3": "^3.5.6",
+ "d3-plugins-sankey": "1.2.1",
"elasticsearch": "^8.0.1",
"elasticsearch-browser": "^8.0.1",
"expiry-js": "^0.1.7",
diff --git a/src/plugins/kbn_vislib_vis_types/public/editors/sankey.html b/src/plugins/kbn_vislib_vis_types/public/editors/sankey.html
new file mode 100644
index 00000000000000..06369349c4d6d2
--- /dev/null
+++ b/src/plugins/kbn_vislib_vis_types/public/editors/sankey.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js b/src/plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js
index 6aa4e07a0230fd..5f6d1ea3e178a1 100644
--- a/src/plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js
+++ b/src/plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js
@@ -4,5 +4,6 @@ define(function (require) {
visTypes.register(require('plugins/kbn_vislib_vis_types/line'));
visTypes.register(require('plugins/kbn_vislib_vis_types/pie'));
visTypes.register(require('plugins/kbn_vislib_vis_types/area'));
+ visTypes.register(require('plugins/kbn_vislib_vis_types/sankey'));
visTypes.register(require('plugins/kbn_vislib_vis_types/tileMap'));
});
diff --git a/src/plugins/kbn_vislib_vis_types/public/sankey.js b/src/plugins/kbn_vislib_vis_types/public/sankey.js
new file mode 100644
index 00000000000000..ade07eedaff883
--- /dev/null
+++ b/src/plugins/kbn_vislib_vis_types/public/sankey.js
@@ -0,0 +1,55 @@
+define(function (require) {
+ return function HistogramVisType(Private) {
+ var VislibVisType = Private(require('ui/vislib_vis_type/VislibVisType'));
+ var Schemas = Private(require('ui/Vis/Schemas'));
+ var sankeyBuilder = Private(require('ui/agg_response/sankey/sankey'));
+
+ return new VislibVisType({
+ name: 'sankey',
+ title: 'Sankey chart',
+ icon: 'fa-sankey-chart',
+ description: 'Sankey charts are ideal for displaying the material, energy and cost flows.' +
+ 'Pro Tip: Sankey charts are best used sparingly, and with no more than 7 slices per sankey.',
+ params: {
+ defaults: {
+ shareYAxis: false,
+ isDonut: false
+ },
+ editor: require('plugins/kbn_vislib_vis_types/editors/sankey.html')
+ },
+ sankeyConverter: sankeyBuilder,
+ hierarchicalData: false,
+ schemas: new Schemas([
+ {
+ group: 'metrics',
+ name: 'metric',
+ title: 'Slice Size',
+ min: 1,
+ aggFilter: ['sum', 'count', 'cardinality', 'min', 'max', 'avg'],
+ defaults: [
+ { schema: 'metric', type: 'count' }
+ ]
+ },
+ {
+ group: 'buckets',
+ name: 'segment',
+ icon: 'fa fa-scissors',
+ title: 'Split Slices',
+ min: 0,
+ max: Infinity,
+ aggFilter: '!geohash_grid'
+ },
+ {
+ group: 'buckets',
+ name: 'split',
+ icon: 'fa fa-th',
+ title: 'Split Chart',
+ mustBeFirst: true,
+ min: 0,
+ max: 1,
+ aggFilter: '!geohash_grid'
+ }
+ ])
+ });
+ };
+});
diff --git a/src/ui/public/Vis/Vis.js b/src/ui/public/Vis/Vis.js
index 79f7b6bcf5d1b1..9794c745312feb 100644
--- a/src/ui/public/Vis/Vis.js
+++ b/src/ui/public/Vis/Vis.js
@@ -113,6 +113,14 @@ define(function (require) {
}
};
+ Vis.prototype.isSankey = function () {
+ if (_.isFunction(this.type.sankeyConverter)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
Vis.prototype.hasSchemaAgg = function (schemaName, aggTypeName) {
var aggs = this.aggs.bySchemaName[schemaName] || [];
return aggs.some(function (agg) {
diff --git a/src/ui/public/Vis/__tests__/_Vis.js b/src/ui/public/Vis/__tests__/_Vis.js
index f72e69e29af9d4..01868027c7ed2f 100644
--- a/src/ui/public/Vis/__tests__/_Vis.js
+++ b/src/ui/public/Vis/__tests__/_Vis.js
@@ -104,4 +104,23 @@ describe('Vis Class', function () {
});
});
+ describe('isSankey()', function () {
+ it('should return true for sankey vis', function () {
+ var stateFixture = {
+ type: 'sankey',
+ aggs: [
+ { type: 'count', schema: 'metric' },
+ { type: 'terms', schema: 'segment', params: { field: 'extension' }},
+ { type: 'terms', schema: 'segment', params: { field: 'machine.os' }},
+ { type: 'terms', schema: 'segment', params: { field: 'geo.src' }}
+ ]
+ };
+ var vis = new Vis(indexPattern, stateFixture);
+ expect(vis.isSankey()).to.be(true);
+ });
+ it('should return false for non-sankey vis (like pie)', function () {
+ expect(vis.isSankey()).to.be(false);
+ });
+ });
+
});
diff --git a/src/ui/public/agg_response/index.js b/src/ui/public/agg_response/index.js
index 1d9aeecc101245..7a5785d4c79bc4 100644
--- a/src/ui/public/agg_response/index.js
+++ b/src/ui/public/agg_response/index.js
@@ -4,6 +4,7 @@ define(function (require) {
hierarchical: Private(require('ui/agg_response/hierarchical/build_hierarchical_data')),
pointSeries: Private(require('ui/agg_response/point_series/point_series')),
tabify: Private(require('ui/agg_response/tabify/tabify')),
+ sankey: Private(require('ui/agg_response/sankey/sankey')),
geoJson: Private(require('ui/agg_response/geo_json/geo_json'))
};
};
diff --git a/src/ui/public/agg_response/sankey/__tests__/sankey.js b/src/ui/public/agg_response/sankey/__tests__/sankey.js
new file mode 100644
index 00000000000000..4ab3805ca089cf
--- /dev/null
+++ b/src/ui/public/agg_response/sankey/__tests__/sankey.js
@@ -0,0 +1,75 @@
+
+var _ = require('lodash');
+var fixtures = require('fixtures/fake_hierarchical_data');
+var sinon = require('auto-release-sinon');
+var expect = require('expect.js');
+var ngMock = require('ngMock');
+
+var Vis;
+var Notifier;
+var AggConfigs;
+var indexPattern;
+var buildSankey;
+
+describe('sankey', function () {
+
+ beforeEach(ngMock.module('kibana'));
+ beforeEach(ngMock.inject(function (Private, $injector) {
+ Notifier = $injector.get('Notifier');
+ sinon.stub(Notifier.prototype, 'error');
+
+ Vis = Private(require('ui/Vis'));
+ AggConfigs = Private(require('ui/Vis/AggConfigs'));
+ indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
+ buildSankey = Private(require('ui/agg_response/sankey/sankey'));
+ }));
+
+ describe('threeTermBuckets', function () {
+ var vis;
+ var results;
+
+ beforeEach(function () {
+ var id = 1;
+ vis = new Vis(indexPattern, {
+ type: 'sankey',
+ aggs: [
+ { type: 'count', schema: 'metric' },
+ { type: 'terms', schema: 'segment', params: { field: 'extension' }},
+ { type: 'terms', schema: 'segment', params: { field: 'machine.os' }},
+ { type: 'terms', schema: 'segment', params: { field: 'geo.src' }}
+ ]
+ });
+ // We need to set the aggs to a known value.
+ _.each(vis.aggs, function (agg) { agg.id = 'agg_' + id++; });
+ results = buildSankey(vis, fixtures.threeTermBuckets);
+ });
+
+ it('should have nodes and links attributes for the results', function () {
+ expect(results).to.have.property('slices');
+ expect(results.slices).to.have.property('nodes');
+ expect(results.slices).to.have.property('links');
+ });
+
+ it('should have name attributes for the nodes array', function () {
+ expect(results.slices.nodes).to.have.length(11);
+ _.each(results.slices.nodes, function (item) {
+ expect(item).to.have.property('name');
+ });
+ expect(results.slices.nodes[0].name).to.equal('png');
+ });
+
+ it('should have source, target and value attributes for the links array', function () {
+ expect(results.slices.links).to.have.length(16);
+ _.each(results.slices.links, function (item) {
+ expect(item).to.have.property('source');
+ expect(item).to.have.property('target');
+ expect(item).to.have.property('value');
+ });
+ expect(results.slices.links[0].source).to.equal(0);
+ expect(results.slices.links[0].target).to.equal(1);
+ expect(results.slices.links[0].value).to.equal(10);
+ });
+
+ });
+
+});
diff --git a/src/ui/public/agg_response/sankey/sankey.js b/src/ui/public/agg_response/sankey/sankey.js
new file mode 100644
index 00000000000000..9f07cf5eae3bd7
--- /dev/null
+++ b/src/ui/public/agg_response/sankey/sankey.js
@@ -0,0 +1,64 @@
+define(function (require) {
+ return function sankeyProvider(Private, Notifier) {
+ var _ = require('lodash');
+ var arrayToLinkedList = require('ui/agg_response/hierarchical/_array_to_linked_list');
+ var notify = new Notifier({
+ location: 'Sankey chart response converter'
+ });
+ var nodes = {};
+ var links = {};
+ var lastNode = -1;
+
+ function processEntry(aggConfig, metric, aggData, prevNode) {
+ _.each(aggData.buckets, function (b) {
+ if (isNaN(nodes[b.key])) {
+ nodes[b.key] = lastNode + 1;
+ lastNode = _.max(_.values(nodes));
+ }
+ if (aggConfig._previous) {
+ var k = prevNode + 'sankeysplitchar' + nodes[b.key];
+ if (isNaN(links[k])) {
+ links[k] = metric.getValue(b);
+ } else {
+ links[k] += metric.getValue(b);
+ }
+ }
+ if (aggConfig._next) {
+ processEntry(aggConfig._next, metric, b[aggConfig._next.id], nodes[b.key]);
+ }
+ });
+ }
+
+ return function (vis, resp) {
+
+ var metric = vis.aggs.bySchemaGroup.metrics[0];
+ var buckets = vis.aggs.bySchemaGroup.buckets;
+ buckets = arrayToLinkedList(buckets);
+ if (!buckets) {
+ return {'slices':{'nodes':[],'links':[]}};
+ }
+
+ var firstAgg = buckets[0];
+ var aggData = resp.aggregations[firstAgg.id];
+
+ if (!firstAgg._next) {
+ notify.error('need more than one sub aggs');
+ }
+
+ processEntry(firstAgg, metric, aggData, -1);
+
+ var invertNodes = _.invert(nodes);
+ var chart = {
+ 'slices': {
+ 'nodes' : _.map(_.keys(invertNodes), function (k) { return {'name':invertNodes[k]}; }),
+ 'links' : _.map(_.keys(links), function (k) {
+ var s = k.split('sankeysplitchar');
+ return {'source': parseInt(s[0]), 'target': parseInt(s[1]), 'value': links[k]};
+ })
+ }
+ };
+
+ return chart;
+ };
+ };
+});
diff --git a/src/ui/public/vislib/__tests__/visualizations/sankey_chart.js b/src/ui/public/vislib/__tests__/visualizations/sankey_chart.js
new file mode 100644
index 00000000000000..4b165e7cc56d3a
--- /dev/null
+++ b/src/ui/public/vislib/__tests__/visualizations/sankey_chart.js
@@ -0,0 +1,59 @@
+var d3 = require('d3');
+var angular = require('angular');
+var expect = require('expect.js');
+var ngMock = require('ngMock');
+var _ = require('lodash');
+var $ = require('jquery');
+var fixtures = require('fixtures/fake_hierarchical_data');
+
+var sliceAgg = [
+ { type: 'count', schema: 'metric' },
+ { type: 'terms', schema: 'segment', params: { field: 'extension' }},
+ { type: 'terms', schema: 'segment', params: { field: 'machine.os' }},
+ { type: 'terms', schema: 'segment', params: { field: 'geo.src' }}
+];
+
+describe('Vislib SankeyChart Class Test Suite for slice data', function () {
+ var visLibParams = {
+ type: 'sankey'
+ };
+ var vis;
+ var Vis;
+ var indexPattern;
+ var buildSankey;
+ var data;
+
+ beforeEach(ngMock.module('kibana'));
+ beforeEach(ngMock.inject(function (Private) {
+ vis = Private(require('fixtures/vislib/_vis_fixture'))(visLibParams);
+ Vis = Private(require('ui/Vis'));
+ indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
+ buildSankey = Private(require('ui/agg_response/sankey/sankey'));
+
+ var id = 1;
+ var stubVis = new Vis(indexPattern, {
+ type: 'sankey',
+ aggs: sliceAgg
+ });
+
+ _.each(stubVis.aggs, function (agg) { agg.id = 'agg_' + id++; });
+
+ data = buildSankey(stubVis, fixtures.threeTermBuckets);
+
+ vis.render(data);
+ }));
+
+ afterEach(function () {
+ $(vis.el).remove();
+ vis = null;
+ });
+
+ describe('draw method', function () {
+ it('should return a function', function () {
+ vis.handler.charts.forEach(function (chart) {
+ expect(_.isFunction(chart.draw())).to.be(true);
+ });
+ });
+ });
+
+});
diff --git a/src/ui/public/vislib/lib/data.js b/src/ui/public/vislib/lib/data.js
index 276c1c3f913708..40f5fb17813d25 100644
--- a/src/ui/public/vislib/lib/data.js
+++ b/src/ui/public/vislib/lib/data.js
@@ -661,6 +661,25 @@ define(function (require) {
}));
};
+ /**
+ * Returns a function that does color lookup on names for sankey charts
+ *
+ * @method getSankeyColorFunc
+ * @returns {Function} Performs lookup on string and returns hex color
+ */
+ Data.prototype.getSankeyColorFunc = function () {
+ var data = this.getVisData();
+ var names = [];
+
+ _.forEach(data, function (obj) {
+ _.forEach(obj.slices.nodes, function (node) {
+ names.push(node.name);
+ });
+ });
+
+ return color(names);
+ };
+
/**
* ensure that the datas ordered property has a min and max
* if the data represents an ordered date range.
diff --git a/src/ui/public/vislib/lib/handler/handler_types.js b/src/ui/public/vislib/lib/handler/handler_types.js
index 9d9c160285865b..75063dd7ff0835 100644
--- a/src/ui/public/vislib/lib/handler/handler_types.js
+++ b/src/ui/public/vislib/lib/handler/handler_types.js
@@ -12,6 +12,7 @@ define(function (require) {
line: pointSeries.line,
pie: Private(require('ui/vislib/lib/handler/types/pie')),
area: pointSeries.area,
+ sankey: Private(require('ui/vislib/lib/handler/types/sankey')),
tile_map: Private(require('ui/vislib/lib/handler/types/tile_map'))
};
};
diff --git a/src/ui/public/vislib/lib/handler/types/sankey.js b/src/ui/public/vislib/lib/handler/types/sankey.js
new file mode 100644
index 00000000000000..96c37d1cc3fa67
--- /dev/null
+++ b/src/ui/public/vislib/lib/handler/types/sankey.js
@@ -0,0 +1,16 @@
+define(function (require) {
+ return function sankeyHandler(Private) {
+ var Handler = Private(require('ui/vislib/lib/handler/handler'));
+ var Data = Private(require('ui/vislib/lib/data'));
+
+ return function (vis) {
+ var data = new Data(vis.data, vis._attr);
+
+ var sankeyHandler = new Handler(vis, {
+ data: data
+ });
+
+ return sankeyHandler;
+ };
+ };
+});
diff --git a/src/ui/public/vislib/lib/layout/layout_types.js b/src/ui/public/vislib/lib/layout/layout_types.js
index ccaa776c4fe9a4..9037d082895cd4 100644
--- a/src/ui/public/vislib/lib/layout/layout_types.js
+++ b/src/ui/public/vislib/lib/layout/layout_types.js
@@ -14,6 +14,7 @@ define(function (require) {
line: Private(require('ui/vislib/lib/layout/types/column_layout')),
area: Private(require('ui/vislib/lib/layout/types/column_layout')),
pie: Private(require('ui/vislib/lib/layout/types/pie_layout')),
+ sankey: Private(require('ui/vislib/lib/layout/types/sankey_layout')),
tile_map: Private(require('ui/vislib/lib/layout/types/map_layout'))
};
};
diff --git a/src/ui/public/vislib/lib/layout/splits/sankey/sankey_split.js b/src/ui/public/vislib/lib/layout/splits/sankey/sankey_split.js
new file mode 100644
index 00000000000000..32969a5ef7c606
--- /dev/null
+++ b/src/ui/public/vislib/lib/layout/splits/sankey/sankey_split.js
@@ -0,0 +1,50 @@
+define(function () {
+ return function ChartSplitFactory() {
+ var d3 = require('d3');
+
+ /*
+ * Adds div DOM elements to the `.chart-wrapper` element based on the data layout.
+ * For example, if the data has rows, it returns the same number of
+ * `.chart` elements as row objects.
+ */
+ return function split(selection) {
+ selection.each(function (data) {
+ var div = d3.select(this)
+ .attr('class', function () {
+ if (data.rows) {
+ return 'chart-wrapper-row';
+ } else if (data.columns) {
+ return 'chart-wrapper-column';
+ } else {
+ return 'chart-wrapper';
+ }
+ });
+ var divClass;
+
+ var charts = div.selectAll('charts')
+ .append('div')
+ .data(function (d) {
+ if (d.rows) {
+ divClass = 'chart-row';
+ return d.rows;
+ } else if (d.columns) {
+ divClass = 'chart-column';
+ return d.columns;
+ } else {
+ divClass = 'chart';
+ return [d];
+ }
+ })
+ .enter()
+ .append('div')
+ .attr('class', function () {
+ return divClass;
+ });
+
+ if (!data.slices) {
+ charts.call(split);
+ }
+ });
+ };
+ };
+});
diff --git a/src/ui/public/vislib/lib/layout/types/sankey_layout.js b/src/ui/public/vislib/lib/layout/types/sankey_layout.js
new file mode 100644
index 00000000000000..e85da08abe34b2
--- /dev/null
+++ b/src/ui/public/vislib/lib/layout/types/sankey_layout.js
@@ -0,0 +1,34 @@
+define(function (require) {
+ return function ColumnLayoutFactory(Private) {
+ var d3 = require('d3');
+ var sankeySplit =
+ Private(require('ui/vislib/lib/layout/splits/sankey/sankey_split'));
+ return function (el, data) {
+ if (!el || !data) {
+ throw new Error('Both an el and data need to be specified');
+ }
+
+ return [
+ {
+ parent: el,
+ type: 'div',
+ class: 'vis-wrapper',
+ datum: data,
+ children: [
+ {
+ type: 'div',
+ class: 'vis-col-wrapper',
+ children: [
+ {
+ type: 'div',
+ class: 'chart-wrapper',
+ splits: sankeySplit
+ }
+ ]
+ }
+ ]
+ }
+ ];
+ };
+ };
+});
diff --git a/src/ui/public/vislib/styles/_sankey.less b/src/ui/public/vislib/styles/_sankey.less
new file mode 100644
index 00000000000000..96f0d4cca8f7f9
--- /dev/null
+++ b/src/ui/public/vislib/styles/_sankey.less
@@ -0,0 +1,20 @@
+.node rect {
+ cursor: move;
+ fill-opacity: .9;
+ shape-rendering: crispEdges;
+}
+
+.node text {
+ pointer-events: none;
+ text-shadow: 0 1px 0 #fff;
+}
+
+.link {
+ fill: none;
+ stroke: #000;
+ stroke-opacity: .2;
+}
+
+.link:hover {
+ stroke-opacity: .5;
+}
diff --git a/src/ui/public/vislib/styles/main.less b/src/ui/public/vislib/styles/main.less
index f44fe6823d33c8..3dd1746fe7368a 100644
--- a/src/ui/public/vislib/styles/main.less
+++ b/src/ui/public/vislib/styles/main.less
@@ -7,3 +7,4 @@
@import "./_tooltip";
@import "./_tilemap";
@import "./_alerts";
+@import "./_sankey";
diff --git a/src/ui/public/vislib/visualizations/sankey_chart.js b/src/ui/public/vislib/visualizations/sankey_chart.js
new file mode 100644
index 00000000000000..b53c80c045f562
--- /dev/null
+++ b/src/ui/public/vislib/visualizations/sankey_chart.js
@@ -0,0 +1,144 @@
+define(function (require) {
+ return function SankeyChartFactory(Private) {
+ var d3 = require('d3');
+ var _ = require('lodash');
+ var $ = require('jquery');
+
+ var S = require('d3-plugins-sankey');
+ var formatNumber = d3.format(',.0f');
+ var format = function (d) { return formatNumber(d) + ' TWh'; };
+
+ var Chart = Private(require('ui/vislib/visualizations/_chart'));
+ var errors = require('ui/errors');
+
+ /**
+ * Sankey Chart Visualization
+ *
+ * @class SankeyChart
+ * @constructor
+ * @extends Chart
+ * @param handler {Object} Reference to the Handler Class Constructor
+ * @param el {HTMLElement} HTML element to which the chart will be appended
+ * @param chartData {Object} Elasticsearch query results for this specific chart
+ */
+ _.class(SankeyChart).inherits(Chart);
+ function SankeyChart(handler, chartEl, chartData) {
+ if (!(this instanceof SankeyChart)) {
+ return new SankeyChart(handler, chartEl, chartData);
+ }
+ SankeyChart.Super.apply(this, arguments);
+
+ var charts = this.handler.data.getVisData();
+ }
+
+
+ SankeyChart.prototype._validateContainerSize = function (width, height) {
+ var minWidth = 20;
+ var minHeight = 20;
+
+ if (width <= minWidth || height <= minHeight) {
+ throw new errors.ContainerTooSmall();
+ }
+ };
+
+ /**
+ * Renders d3 visualization
+ *
+ * @method draw
+ * @returns {Function} Creates the sankey chart
+ */
+ SankeyChart.prototype.draw = function () {
+ var self = this;
+ var $elem = $(this.chartEl);
+ var margin = this._attr.margin;
+ var elWidth = this._attr.width = $elem.width();
+ var elHeight = this._attr.height = $elem.height();
+ var color = this.handler.data.getSankeyColorFunc();
+ var width;
+ var height;
+ var div;
+ var svg;
+
+ return function (selection) {
+ selection.each(function (data) {
+ var energy = data.slices;
+ div = d3.select(this);
+ width = elWidth - margin.left - margin.right;
+ height = elHeight - margin.top - margin.bottom;
+
+ if (!energy.nodes.length) return;
+
+ self._validateContainerSize(width, height);
+
+ svg = div.append('svg')
+ .attr('width', elWidth)
+ .attr('height', elHeight)
+ .append('g')
+ .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+
+ var sankey = d3.sankey()
+ .nodeWidth(15)
+ .nodePadding(10)
+ .size([width, height]);
+
+ var path = sankey.link();
+
+ sankey
+ .nodes(energy.nodes)
+ .links(energy.links)
+ .layout(32);
+
+ var link = svg.append('g').selectAll('.link')
+ .data(energy.links)
+ .enter().append('path')
+ .attr('class', 'link')
+ .attr('d', path)
+ .style('stroke-width', function (d) { return Math.max(1, d.dy); })
+ .sort(function (a, b) { return b.dy - a.dy; });
+
+ link.append('title')
+ .text(function (d) { return d.source.name + ' → ' + d.target.name + '\n' + format(d.value); });
+
+ var node = svg.append('g').selectAll('.node')
+ .data(energy.nodes)
+ .enter().append('g')
+ .attr('class', 'node')
+ .attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
+ .call(d3.behavior.drag()
+ .origin(function (d) { return d; })
+ .on('dragstart', function () { this.parentNode.appendChild(this); })
+ .on('drag', dragmove));
+
+ node.append('rect')
+ .attr('height', function (d) { return d.dy; })
+ .attr('width', sankey.nodeWidth())
+ .style('fill', function (d) { return d.color = color(d.name); })
+ .style('stroke', function (d) { return d3.rgb(d.color).darker(2); })
+ .append('title')
+ .text(function (d) { return d.name + '\n' + format(d.value); });
+
+ node.append('text')
+ .attr('x', -6)
+ .attr('y', function (d) { return d.dy / 2; })
+ .attr('dy', '.35em')
+ .attr('text-anchor', 'end')
+ .attr('transform', null)
+ .text(function (d) { return d.name; })
+ .filter(function (d) { return d.x < width / 2; })
+ .attr('x', 6 + sankey.nodeWidth())
+ .attr('text-anchor', 'start');
+
+ function dragmove(d) {
+ d3.select(this).attr('transform', 'translate(' + d.x + ',' + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ')');
+ sankey.relayout();
+ link.attr('d', path);
+ }
+
+ return svg;
+ });
+ };
+ };
+
+ return SankeyChart;
+ };
+});
diff --git a/src/ui/public/vislib/visualizations/vis_types.js b/src/ui/public/vislib/visualizations/vis_types.js
index cc238529043049..1a8ee50eeac41a 100644
--- a/src/ui/public/vislib/visualizations/vis_types.js
+++ b/src/ui/public/vislib/visualizations/vis_types.js
@@ -14,6 +14,7 @@ define(function (require) {
pie: Private(require('ui/vislib/visualizations/pie_chart')),
line: Private(require('ui/vislib/visualizations/line_chart')),
area: Private(require('ui/vislib/visualizations/area_chart')),
+ sankey: Private(require('ui/vislib/visualizations/sankey_chart')),
tile_map: Private(require('ui/vislib/visualizations/tile_map'))
};
};
diff --git a/src/ui/public/vislib_vis_type/VislibVisType.js b/src/ui/public/vislib_vis_type/VislibVisType.js
index 32dbb8503d99d8..9d41701ba66e0f 100644
--- a/src/ui/public/vislib_vis_type/VislibVisType.js
+++ b/src/ui/public/vislib_vis_type/VislibVisType.js
@@ -23,6 +23,7 @@ define(function (require) {
}
this.listeners = opts.listeners || {};
+ this.sankeyConverter = opts.sankeyConverter || false;
}
VislibVisType.prototype.createRenderbot = function (vis, $el) {
diff --git a/src/ui/public/vislib_vis_type/__tests__/_buildChartData.js b/src/ui/public/vislib_vis_type/__tests__/_buildChartData.js
index a623a83d08e9a9..2426128fc440f1 100644
--- a/src/ui/public/vislib_vis_type/__tests__/_buildChartData.js
+++ b/src/ui/public/vislib_vis_type/__tests__/_buildChartData.js
@@ -24,6 +24,7 @@ describe('renderbot#buildChartData', function () {
var football = {};
var renderbot = {
vis: {
+ isSankey: _.constant(false),
isHierarchical: _.constant(true)
}
};
@@ -40,6 +41,7 @@ describe('renderbot#buildChartData', function () {
it('calls tabify to simplify the data into a table', function () {
var renderbot = {
vis: {
+ isSankey: _.constant(false),
isHierarchical: _.constant(false)
}
};
@@ -56,6 +58,7 @@ describe('renderbot#buildChartData', function () {
var chart = { hits: 1, rows: [], columns: [] };
var renderbot = {
vis: {
+ isSankey: _.constant(false),
isHierarchical: _.constant(false),
type: {
responseConverter: _.constant(chart)
@@ -77,6 +80,7 @@ describe('renderbot#buildChartData', function () {
var renderbot = {
vis: {
+ isSankey: _.constant(false),
isHierarchical: _.constant(false),
type: {
responseConverter: converter
diff --git a/src/ui/public/vislib_vis_type/buildChartData.js b/src/ui/public/vislib_vis_type/buildChartData.js
index 29f956f9e9693c..458e0a45aba707 100644
--- a/src/ui/public/vislib_vis_type/buildChartData.js
+++ b/src/ui/public/vislib_vis_type/buildChartData.js
@@ -11,6 +11,10 @@ define(function (require) {
return aggResponse.hierarchical(vis, esResponse);
}
+ if (vis.isSankey()) {
+ return aggResponse.sankey(vis, esResponse);
+ }
+
var tableGroup = aggResponse.tabify(vis, esResponse, {
canSplit: true,
asAggConfigResults: true