mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Merge branch 'master' into template_vistype
Conflicts: src/kibana/plugins/vis_types/index.js test/unit/index.html
This commit is contained in:
commit
2aa654a8c1
24 changed files with 754 additions and 83 deletions
|
@ -857,6 +857,6 @@ require('routes')
|
|||
|
||||
# Attribution
|
||||
|
||||
This Javascript guide forked from the [node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) and is
|
||||
This JavaScript guide forked from the [node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) and is
|
||||
licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/)
|
||||
license.
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
return function (leaf) {
|
||||
// walk up the branch for each parent
|
||||
function walk(item, memo) {
|
||||
// record the the depth
|
||||
var depth = item.depth - 1;
|
||||
|
||||
// Using the aggConfig determin what the field name is. If the aggConfig
|
||||
// doesn't exist (which means it's an _all agg) then use the level for
|
||||
// the field name
|
||||
var col = item.aggConfig;
|
||||
var field = (col && col.params && col.params.field && col.params.field.name)
|
||||
|| (col && col.label)
|
||||
|| ('level ' + item.depth);
|
||||
|
||||
// Set the bucket name, and use the converter to format the field if
|
||||
// the field exists.
|
||||
var bucket = item.name;
|
||||
if (col && col.field && col.field.format && col.field.format.convert) {
|
||||
bucket = col.field.format.convert(bucket);
|
||||
}
|
||||
|
||||
// Add the row to the tooltipScope.rows
|
||||
memo.unshift({
|
||||
aggConfig: col,
|
||||
depth: depth,
|
||||
field: field,
|
||||
bucket: bucket,
|
||||
metric: item.value
|
||||
});
|
||||
|
||||
// If the item has a parent and it's also a child then continue walking
|
||||
// up the branch
|
||||
if (item.parent && item.parent.parent) {
|
||||
return walk(item.parent, memo);
|
||||
} else {
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
return walk(leaf, []);
|
||||
};
|
||||
});
|
|
@ -3,6 +3,7 @@ define(function (require) {
|
|||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var $tooltip = $(require('text!plugins/vis_types/tooltips/pie.html'));
|
||||
var collectBranch = require('components/visualize/_collect_branch');
|
||||
var $tooltipScope = $rootScope.$new();
|
||||
$compile($tooltip)($tooltipScope);
|
||||
|
||||
|
@ -18,40 +19,15 @@ define(function (require) {
|
|||
sum = parent.value;
|
||||
}
|
||||
|
||||
var rows = $tooltipScope.rows = [];
|
||||
// Collect the current leaf and parents into an array of values
|
||||
var rows = collectBranch(datum);
|
||||
|
||||
// walk up the branch for each parent
|
||||
(function walk(item) {
|
||||
// record the the depth
|
||||
var i = item.depth - 1;
|
||||
|
||||
// Using the aggConfig determin what the field name is. If the aggConfig
|
||||
// doesn't exist (which means it's an _all agg) then use the level for
|
||||
// the field name
|
||||
var col = item.aggConfig;
|
||||
var field = (col && col.params && col.params.field && col.params.field.name)
|
||||
|| (col && col.label)
|
||||
|| ('level ' + datum.depth);
|
||||
|
||||
// Set the bucket name, and use the converter to format the field if
|
||||
// the field exists.
|
||||
var bucket = item.name;
|
||||
if (col && col.field) bucket = col.field.format.convert(bucket);
|
||||
|
||||
// Add the row to the tooltipScope.rows
|
||||
rows.unshift({
|
||||
spacer: $sce.trustAsHtml(_.repeat(' ', i)),
|
||||
field: field,
|
||||
bucket: bucket,
|
||||
metric: item.value + ' (' + Math.round((item.value / sum) * 100) + '%)'
|
||||
});
|
||||
|
||||
// If the item has a parent and it's also a child then continue walking
|
||||
// up the branch
|
||||
if (item.parent && item.parent.parent) {
|
||||
walk(item.parent);
|
||||
}
|
||||
})(datum);
|
||||
// Map those values to what the tooltipSource.rows format.
|
||||
$tooltipScope.rows = _.map(rows, function (row) {
|
||||
row.spacer = $sce.trustAsHtml(_.repeat(' ', row.depth));
|
||||
row.metric = row.metric + ' (' + Math.round((row.metric / sum) * 100) + '%)';
|
||||
return row;
|
||||
});
|
||||
|
||||
$tooltipScope.metricCol = _.find(columns, { categoryName: 'metric' });
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ define(function (require) {
|
|||
var _ = require('lodash');
|
||||
var typeahead = require('modules').get('kibana/typeahead');
|
||||
|
||||
require('css!components/typeahead/typeahead.css');
|
||||
require('components/typeahead/_input');
|
||||
require('components/typeahead/_items');
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
@import (reference) "../../styles/_bootstrap.less";
|
||||
@import (reference) "../../styles/theme/_theme.less";
|
||||
@import (reference) "../../styles/_variables.less";
|
||||
|
||||
.typeahead {
|
||||
position: relative;
|
||||
|
|
@ -21,16 +21,28 @@ define(function (require) {
|
|||
return new Data(data, attr);
|
||||
}
|
||||
|
||||
var offset;
|
||||
|
||||
if (attr.mode === 'stacked') {
|
||||
offset = 'zero';
|
||||
} else if (attr.mode === 'percentage') {
|
||||
offset = 'expand';
|
||||
} else if (attr.mode === 'grouped') {
|
||||
offset = 'group';
|
||||
} else {
|
||||
offset = attr.mode;
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
this._normalizeOrdered();
|
||||
|
||||
this._attr = attr;
|
||||
this._attr = _.defaults(attr || {}, {
|
||||
offset: 'zero',
|
||||
|
||||
// d3 stack function
|
||||
stack: d3.layout.stack()
|
||||
.x(function (d) { return d.x; })
|
||||
.y(function (d) { return d.y; })
|
||||
.offset(this._attr.offset)
|
||||
.offset(offset || 'zero')
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -83,8 +95,9 @@ define(function (require) {
|
|||
var seriesLabel;
|
||||
|
||||
_.forEach(visData, function countSeriesLength(obj) {
|
||||
var dataLength = obj.series ? obj.series.length : obj.slices.children.length;
|
||||
var label = (dataLength === 1 && obj.series) ? obj.series[0].label : undefined;
|
||||
var rootSeries = obj.series || (obj.slices && obj.slices.children);
|
||||
var dataLength = rootSeries ? rootSeries.length : 0;
|
||||
var label = dataLength === 1 ? rootSeries[0].label : undefined;
|
||||
|
||||
if (!seriesLabel) {
|
||||
seriesLabel = label;
|
||||
|
@ -168,8 +181,12 @@ define(function (require) {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
Data.prototype.shouldBeStacked = function (series) {
|
||||
var isHistogram = (this._attr.type === 'histogram');
|
||||
var isArea = (this._attr.type === 'area');
|
||||
var isOverlapping = (this._attr.mode === 'overlap');
|
||||
|
||||
// Series should be an array
|
||||
return (this._attr.type === 'histogram' && series.length > 1);
|
||||
return (isHistogram || isArea && !isOverlapping && series.length > 1);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -184,9 +201,20 @@ define(function (require) {
|
|||
Data.prototype.getYMaxValue = function () {
|
||||
var self = this;
|
||||
var arr = [];
|
||||
var grouped = (self._attr.mode === 'grouped');
|
||||
|
||||
if (self._attr.mode === 'percentage') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (self._attr.mode === 'percentage') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// for each object in the dataArray,
|
||||
// push the calculated y value to the initialized array (arr)
|
||||
_.forEach(this.flatten(), function (series) {
|
||||
if (self.shouldBeStacked(series)) {
|
||||
if (self.shouldBeStacked(series) && !grouped) {
|
||||
return arr.push(self.getYStackMax(series));
|
||||
}
|
||||
return arr.push(self.getYMax(series));
|
||||
|
|
|
@ -21,9 +21,7 @@ define(function (require) {
|
|||
this.chartData = chartData;
|
||||
this.color = type === 'pie' ? handler.data.getPieColorFunc() : handler.data.getColorFunc();
|
||||
this._attr = _.defaults(handler._attr || {}, {
|
||||
yValue: function (d) {
|
||||
return d.y;
|
||||
},
|
||||
yValue: function (d) { return d.y; },
|
||||
dispatch: d3.dispatch('brush', 'click', 'hover', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout')
|
||||
});
|
||||
}
|
||||
|
@ -37,6 +35,7 @@ define(function (require) {
|
|||
* e: (d3.event|*), handler: (Object|*)}} Event response object
|
||||
*/
|
||||
Dispatch.prototype.eventResponse = function (d, i) {
|
||||
var isPercentage = (this._attr.mode === 'percentage');
|
||||
var label = d.label;
|
||||
var getYValue = this._attr.yValue;
|
||||
var color = this.color;
|
||||
|
@ -44,6 +43,18 @@ define(function (require) {
|
|||
var attr = this._attr;
|
||||
var handler = this.handler;
|
||||
|
||||
if (chartData.series) {
|
||||
// Find object with the actual d value and add it to the point object
|
||||
var object = _.find(chartData.series, { 'label': label });
|
||||
d.value = +object.values[i].y;
|
||||
|
||||
if (isPercentage) {
|
||||
|
||||
// Add the formatted percentage to the point object
|
||||
d.percent = (100 * d.y).toFixed(1) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: getYValue(d, i),
|
||||
point: d,
|
||||
|
|
|
@ -9,6 +9,7 @@ define(function (require) {
|
|||
return {
|
||||
histogram: Private(require('components/vislib/lib/handler/types/column')),
|
||||
line: Private(require('components/vislib/lib/handler/types/column')),
|
||||
area: Private(require('components/vislib/lib/handler/types/column')),
|
||||
pie: Private(require('components/vislib/lib/handler/types/pie'))
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ define(function (require) {
|
|||
return {
|
||||
histogram: Private(require('components/vislib/lib/layout/types/column_layout')),
|
||||
line: Private(require('components/vislib/lib/layout/types/column_layout')),
|
||||
area: Private(require('components/vislib/lib/layout/types/column_layout')),
|
||||
pie: Private(require('components/vislib/lib/layout/types/pie_layout'))
|
||||
};
|
||||
};
|
||||
|
|
|
@ -30,6 +30,7 @@ define(function (require) {
|
|||
'blurredOpacity' : 0.3,
|
||||
'focusOpacity' : 1,
|
||||
'defaultOpacity' : 1,
|
||||
'legendDefaultOpacity': 1,
|
||||
'isOpen' : true
|
||||
});
|
||||
}
|
||||
|
@ -140,10 +141,22 @@ define(function (require) {
|
|||
visEl.selectAll(liClass).style('opacity', self._attr.focusOpacity);
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
visEl.selectAll('.color').style('opacity', self._attr.defaultOpacity);
|
||||
|
||||
/*
|
||||
* The default opacity of elements in charts may be modified by the
|
||||
* chart constructor, and so may differ from that of the legend
|
||||
*/
|
||||
visEl.select('.chart')
|
||||
.selectAll('.color')
|
||||
.style('opacity', self._attr.defaultOpacity);
|
||||
|
||||
// Legend values should always return to their default opacity of 1
|
||||
visEl.select('.legend-ul')
|
||||
.selectAll('.color')
|
||||
.style('opacity', self._attr.legendDefaultOpacity);
|
||||
});
|
||||
};
|
||||
|
||||
return Legend;
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -56,6 +56,16 @@ define(function (require) {
|
|||
*/
|
||||
YAxis.prototype.getYAxis = function (height) {
|
||||
var yScale = this.getYScale(height);
|
||||
var isPercentage = (this._attr.mode === 'percentage');
|
||||
var tickFormat;
|
||||
|
||||
if (isPercentage) {
|
||||
tickFormat = d3.format('%');
|
||||
} else if (height <= 1 && !isPercentage) {
|
||||
tickFormat = d3.format('n');
|
||||
} else {
|
||||
tickFormat = d3.format('s');
|
||||
}
|
||||
|
||||
// y scale should never be `NaN`
|
||||
if (!yScale || _.isNaN(yScale)) {
|
||||
|
@ -64,14 +74,10 @@ define(function (require) {
|
|||
|
||||
// Create the d3 yAxis function
|
||||
this.yAxis = d3.svg.axis()
|
||||
.scale(yScale)
|
||||
.tickFormat(d3.format('s'))
|
||||
.ticks(this.tickScale(height))
|
||||
.orient('left');
|
||||
|
||||
if (this.yScale.domain()[1] <= 10) {
|
||||
this.yAxis.tickFormat(d3.format('n'));
|
||||
}
|
||||
.scale(yScale)
|
||||
.tickFormat(tickFormat)
|
||||
.ticks(this.tickScale(height))
|
||||
.orient('left');
|
||||
|
||||
return this.yAxis;
|
||||
};
|
||||
|
@ -104,6 +110,8 @@ define(function (require) {
|
|||
YAxis.prototype.draw = function () {
|
||||
var self = this;
|
||||
var margin = this._attr.margin;
|
||||
var mode = this._attr.mode;
|
||||
var isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette');
|
||||
var div;
|
||||
var width;
|
||||
var height;
|
||||
|
@ -123,15 +131,18 @@ define(function (require) {
|
|||
|
||||
var yAxis = self.getYAxis(height);
|
||||
|
||||
// Append svg and y axis
|
||||
svg = div.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height + margin.top + margin.bottom);
|
||||
// The yAxis should not appear if mode is set to 'wiggle' or 'silhouette'
|
||||
if (!isWiggleOrSilhouette) {
|
||||
// Append svg and y axis
|
||||
svg = div.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height + margin.top + margin.bottom);
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')')
|
||||
.call(yAxis);
|
||||
svg.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')')
|
||||
.call(yAxis);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
370
src/kibana/components/vislib/visualizations/area_chart.js
Normal file
370
src/kibana/components/vislib/visualizations/area_chart.js
Normal file
|
@ -0,0 +1,370 @@
|
|||
define(function (require) {
|
||||
return function AreaChartFactory(d3, Private) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
var Chart = Private(require('components/vislib/visualizations/_chart'));
|
||||
var errors = require('errors');
|
||||
require('css!components/vislib/styles/main');
|
||||
|
||||
/**
|
||||
* Area chart visualization
|
||||
*
|
||||
* @class AreaChart
|
||||
* @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
|
||||
*/
|
||||
_(AreaChart).inherits(Chart);
|
||||
function AreaChart(handler, chartEl, chartData) {
|
||||
if (!(this instanceof AreaChart)) {
|
||||
return new AreaChart(handler, chartEl, chartData);
|
||||
}
|
||||
|
||||
AreaChart.Super.apply(this, arguments);
|
||||
|
||||
var raw;
|
||||
var fieldIndex;
|
||||
|
||||
if (handler.data.data.raw) {
|
||||
raw = handler.data.data.raw.columns;
|
||||
fieldIndex = _.findIndex(raw, {'categoryName': 'group'});
|
||||
}
|
||||
|
||||
this.isOverlapping = (handler._attr.mode === 'overlap');
|
||||
|
||||
if (this.isOverlapping) {
|
||||
|
||||
// Default opacity should return to 0.6 on mouseout
|
||||
handler._attr.defaultOpacity = 0.6;
|
||||
}
|
||||
|
||||
this.fieldFormatter = (raw && raw[fieldIndex]) ?
|
||||
raw[fieldIndex].field.format.convert : function (d) { return d; };
|
||||
|
||||
this._attr = _.defaults(handler._attr || {}, {
|
||||
xValue: function (d) { return d.x; },
|
||||
yValue: function (d) { return d.y; }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stacks chart data values
|
||||
* TODO: refactor so that this is called from the data module
|
||||
*
|
||||
* @method stackData
|
||||
* @param data {Object} Elasticsearch query result for this chart
|
||||
* @returns {Array} Stacked data objects with x, y, and y0 values
|
||||
*/
|
||||
AreaChart.prototype.stackData = function (data) {
|
||||
var self = this;
|
||||
var stack = this._attr.stack;
|
||||
|
||||
return stack(data.series.map(function (d) {
|
||||
var label = d.label;
|
||||
return d.values.map(function (e, i) {
|
||||
return {
|
||||
label: label,
|
||||
x: self._attr.xValue.call(d.values, e, i),
|
||||
y: self._attr.yValue.call(d.values, e, i)
|
||||
};
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds SVG path to area chart
|
||||
*
|
||||
* @method addPath
|
||||
* @param svg {HTMLElement} SVG to which rect are appended
|
||||
* @param layers {Array} Chart data array
|
||||
* @returns {D3.UpdateSelection} SVG with path added
|
||||
*/
|
||||
AreaChart.prototype.addPath = function (svg, layers) {
|
||||
var self = this;
|
||||
var ordered = this.handler.data.get('ordered');
|
||||
var isTimeSeries = (ordered && ordered.date);
|
||||
var isOverlapping = this.isOverlapping;
|
||||
var color = this.handler.data.getColorFunc();
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var yScale = this.handler.yAxis.yScale;
|
||||
var height = yScale.range()[0];
|
||||
var defaultOpacity = this._attr.defaultOpacity;
|
||||
|
||||
var area = d3.svg.area()
|
||||
.x(function (d) {
|
||||
if (isTimeSeries) {
|
||||
return xScale(d.x);
|
||||
}
|
||||
return xScale(d.x) + xScale.rangeBand() / 2;
|
||||
})
|
||||
.y0(function (d) {
|
||||
if (isOverlapping) {
|
||||
return height;
|
||||
}
|
||||
return yScale(d.y0);
|
||||
})
|
||||
.y1(function (d) {
|
||||
if (isOverlapping) {
|
||||
return yScale(d.y);
|
||||
}
|
||||
return yScale(d.y0 + d.y);
|
||||
});
|
||||
|
||||
var layer;
|
||||
var path;
|
||||
|
||||
// Data layers
|
||||
layer = svg.selectAll('.layer')
|
||||
.data(layers)
|
||||
.enter().append('g')
|
||||
.attr('class', function (d, i) {
|
||||
return i;
|
||||
});
|
||||
|
||||
// Append path
|
||||
path = layer.append('path')
|
||||
.attr('class', function (d) {
|
||||
return self.colorToClass(color(self.fieldFormatter(d[0].label)));
|
||||
})
|
||||
.style('fill', function (d) {
|
||||
return color(self.fieldFormatter(d[0].label));
|
||||
})
|
||||
.style('opacity', defaultOpacity);
|
||||
|
||||
// update
|
||||
path.attr('d', function (d) {
|
||||
return area(d);
|
||||
});
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds Events to SVG circles
|
||||
*
|
||||
* @method addCircleEvents
|
||||
* @param circles {D3.UpdateSelection} SVG circles
|
||||
* @returns {HTMLElement} circles with event listeners attached
|
||||
*/
|
||||
AreaChart.prototype.addCircleEvents = function (circles) {
|
||||
var events = this.events;
|
||||
var dispatch = this.events._attr.dispatch;
|
||||
|
||||
circles
|
||||
.on('mouseover.circle', function mouseOverCircle(d, i) {
|
||||
var circle = this;
|
||||
|
||||
d3.select(circle)
|
||||
.classed('hover', true)
|
||||
.style('stroke', '#333')
|
||||
.style('cursor', 'pointer')
|
||||
.style('opacity', 1);
|
||||
|
||||
dispatch.hover(events.eventResponse(d, i));
|
||||
d3.event.stopPropagation();
|
||||
})
|
||||
.on('click.circle', function clickCircle(d, i) {
|
||||
dispatch.click(events.eventResponse(d, i));
|
||||
d3.event.stopPropagation();
|
||||
})
|
||||
.on('mouseout.circle', function mouseOutCircle() {
|
||||
var circle = this;
|
||||
|
||||
d3.select(circle)
|
||||
.classed('hover', false)
|
||||
.style('stroke', null)
|
||||
.style('opacity', 0);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds SVG circles to area chart
|
||||
*
|
||||
* @method addCircles
|
||||
* @param svg {HTMLElement} SVG to which circles are appended
|
||||
* @param data {Array} Chart data array
|
||||
* @returns {D3.UpdateSelection} SVG with circles added
|
||||
*/
|
||||
AreaChart.prototype.addCircles = function (svg, data) {
|
||||
var self = this;
|
||||
var color = this.handler.data.getColorFunc();
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var yScale = this.handler.yAxis.yScale;
|
||||
var ordered = this.handler.data.get('ordered');
|
||||
var circleRadius = 4;
|
||||
var circleStrokeWidth = 1;
|
||||
var tooltip = this.tooltip;
|
||||
var isTooltip = this._attr.addTooltip;
|
||||
var isOverlapping = this.isOverlapping;
|
||||
var layer;
|
||||
var circles;
|
||||
|
||||
layer = svg.selectAll('.points')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'points');
|
||||
|
||||
// Append the bars
|
||||
circles = layer
|
||||
.selectAll('rect')
|
||||
.data(function appendData(d) {
|
||||
return d;
|
||||
});
|
||||
|
||||
// exit
|
||||
circles.exit().remove();
|
||||
|
||||
// enter
|
||||
circles
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', function circleClass(d) {
|
||||
return d.label;
|
||||
})
|
||||
.attr('fill', function (d) {
|
||||
return color(self.fieldFormatter(d.label));
|
||||
})
|
||||
.attr('stroke', function strokeColor(d) {
|
||||
return color(self.fieldFormatter(d.label));
|
||||
})
|
||||
.attr('stroke-width', circleStrokeWidth);
|
||||
|
||||
// update
|
||||
circles
|
||||
.attr('cx', function cx(d) {
|
||||
if (ordered && ordered.date) {
|
||||
return xScale(d.x);
|
||||
}
|
||||
return xScale(d.x) + xScale.rangeBand() / 2;
|
||||
})
|
||||
.attr('cy', function cy(d) {
|
||||
if (isOverlapping) {
|
||||
return yScale(d.y);
|
||||
}
|
||||
return yScale(d.y0 + d.y);
|
||||
})
|
||||
.attr('r', circleRadius)
|
||||
.style('opacity', 0);
|
||||
|
||||
// Add tooltip
|
||||
if (isTooltip) {
|
||||
circles.call(tooltip.render());
|
||||
}
|
||||
|
||||
return circles;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds SVG clipPath
|
||||
*
|
||||
* @method addClipPath
|
||||
* @param svg {HTMLElement} SVG to which clipPath is appended
|
||||
* @param width {Number} SVG width
|
||||
* @param height {Number} SVG height
|
||||
* @returns {D3.UpdateSelection} SVG with clipPath added
|
||||
*/
|
||||
AreaChart.prototype.addClipPath = function (svg, width, height) {
|
||||
// Prevents circles from being clipped at the top of the chart
|
||||
var clipPathBuffer = 5;
|
||||
var startX = 0;
|
||||
var startY = 0 - clipPathBuffer;
|
||||
var id = 'chart-area' + _.uniqueId();
|
||||
|
||||
// Creating clipPath
|
||||
return svg
|
||||
.attr('clip-path', 'url(#' + id + ')')
|
||||
.append('clipPath')
|
||||
.attr('id', id)
|
||||
.append('rect')
|
||||
.attr('x', startX)
|
||||
.attr('y', startY)
|
||||
.attr('width', width)
|
||||
// Adding clipPathBuffer to height so it doesn't
|
||||
// cutoff the lower part of the chart
|
||||
.attr('height', height + clipPathBuffer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders d3 visualization
|
||||
*
|
||||
* @method draw
|
||||
* @returns {Function} Creates the area chart
|
||||
*/
|
||||
AreaChart.prototype.draw = function () {
|
||||
// Attributes
|
||||
var self = this;
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var $elem = $(this.chartEl);
|
||||
var margin = this._attr.margin;
|
||||
var elWidth = this._attr.width = $elem.width();
|
||||
var elHeight = this._attr.height = $elem.height();
|
||||
var minWidth = 20;
|
||||
var minHeight = 20;
|
||||
var div;
|
||||
var svg;
|
||||
var width;
|
||||
var height;
|
||||
var layers;
|
||||
var circles;
|
||||
var path;
|
||||
|
||||
return function (selection) {
|
||||
selection.each(function (data) {
|
||||
// Stack data
|
||||
layers = self.stackData(data);
|
||||
|
||||
// Get the width and height
|
||||
width = elWidth;
|
||||
height = elHeight - margin.top - margin.bottom;
|
||||
|
||||
if (width < minWidth || height < minHeight) {
|
||||
throw new errors.ContainerTooSmall();
|
||||
}
|
||||
|
||||
// Select the current DOM element
|
||||
div = d3.select(this);
|
||||
|
||||
// Create the canvas for the visualization
|
||||
svg = div.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(0,' + margin.top + ')');
|
||||
|
||||
// add clipPath to hide circles when they go out of bounds
|
||||
self.addClipPath(svg, width, height);
|
||||
|
||||
// addBrush canvas
|
||||
self.events.addBrush(xScale, svg);
|
||||
|
||||
// add path
|
||||
path = self.addPath(svg, layers);
|
||||
|
||||
// add circles
|
||||
circles = self.addCircles(svg, layers);
|
||||
|
||||
// add click and hover events to circles
|
||||
self.addCircleEvents(circles);
|
||||
|
||||
// chart base line
|
||||
var line = svg.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', height)
|
||||
.attr('x2', width)
|
||||
.attr('y2', height)
|
||||
.style('stroke', '#ddd')
|
||||
.style('stroke-width', 1);
|
||||
|
||||
return svg;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return AreaChart;
|
||||
};
|
||||
});
|
|
@ -76,10 +76,7 @@ define(function (require) {
|
|||
*/
|
||||
ColumnChart.prototype.addBars = function (svg, layers) {
|
||||
var self = this;
|
||||
var data = this.chartData;
|
||||
var color = this.handler.data.getColorFunc();
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var yScale = this.handler.yAxis.yScale;
|
||||
var tooltip = this.tooltip;
|
||||
var isTooltip = this._attr.addTooltip;
|
||||
var layer;
|
||||
|
@ -104,13 +101,53 @@ define(function (require) {
|
|||
bars
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', function (d) {
|
||||
return self.colorToClass(color(self.fieldFormatter(d.label)));
|
||||
})
|
||||
.attr('fill', function (d) {
|
||||
return color(self.fieldFormatter(d.label));
|
||||
});
|
||||
.attr('class', function (d) {
|
||||
return self.colorToClass(color(self.fieldFormatter(d.label)));
|
||||
})
|
||||
.attr('fill', function (d) {
|
||||
return color(self.fieldFormatter(d.label));
|
||||
});
|
||||
|
||||
self.updateBars(bars);
|
||||
|
||||
// Add tooltip
|
||||
if (isTooltip) {
|
||||
bars.call(tooltip.render());
|
||||
}
|
||||
|
||||
return bars;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether bars are grouped or stacked and updates the D3
|
||||
* selection
|
||||
*
|
||||
* @method updateBars
|
||||
* @param bars {D3.UpdateSelection} SVG with rect added
|
||||
* @returns {D3.UpdateSelection}
|
||||
*/
|
||||
ColumnChart.prototype.updateBars = function (bars) {
|
||||
var offset = this._attr.mode;
|
||||
|
||||
if (offset === 'grouped') {
|
||||
return this.addGroupedBars(bars);
|
||||
}
|
||||
return this.addStackedBars(bars);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds stacked bars to column chart visualization
|
||||
*
|
||||
* @method addStackedBars
|
||||
* @param bars {D3.UpdateSelection} SVG with rect added
|
||||
* @returns {D3.UpdateSelection}
|
||||
*/
|
||||
ColumnChart.prototype.addStackedBars = function (bars) {
|
||||
var data = this.chartData;
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var yScale = this.handler.yAxis.yScale;
|
||||
|
||||
// update
|
||||
bars
|
||||
.attr('x', function (d) {
|
||||
return xScale(d.x);
|
||||
|
@ -134,9 +171,57 @@ define(function (require) {
|
|||
return yScale(d.y0) - yScale(d.y0 + d.y);
|
||||
});
|
||||
|
||||
if (isTooltip) {
|
||||
bars.call(tooltip.render());
|
||||
}
|
||||
return bars;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds grouped bars to column chart visualization
|
||||
*
|
||||
* @method addGroupedBars
|
||||
* @param bars {D3.UpdateSelection} SVG with rect added
|
||||
* @returns {D3.UpdateSelection}
|
||||
*/
|
||||
ColumnChart.prototype.addGroupedBars = function (bars) {
|
||||
var xScale = this.handler.xAxis.xScale;
|
||||
var yScale = this.handler.yAxis.yScale;
|
||||
var data = this.chartData;
|
||||
var n = data.series.length;
|
||||
var height = yScale.range()[0];
|
||||
var groupSpacingPercentage = 0.15;
|
||||
var isTimeScale = (data.ordered && data.ordered.date);
|
||||
var minWidth = 1;
|
||||
var barWidth;
|
||||
|
||||
// update
|
||||
bars
|
||||
.attr('x', function (d, i, j) {
|
||||
if (isTimeScale) {
|
||||
var groupWidth = xScale(data.ordered.min + data.ordered.interval) -
|
||||
xScale(data.ordered.min);
|
||||
var groupSpacing = groupWidth * groupSpacingPercentage;
|
||||
|
||||
barWidth = (groupWidth - groupSpacing) / n;
|
||||
|
||||
return xScale(d.x) + barWidth * j;
|
||||
}
|
||||
return xScale(d.x) + xScale.rangeBand() / n * j;
|
||||
})
|
||||
.attr('width', function () {
|
||||
if (barWidth < minWidth) {
|
||||
throw new errors.ContainerTooSmall();
|
||||
}
|
||||
|
||||
if (isTimeScale) {
|
||||
return barWidth;
|
||||
}
|
||||
return xScale.rangeBand() / n;
|
||||
})
|
||||
.attr('y', function (d) {
|
||||
return yScale(d.y);
|
||||
})
|
||||
.attr('height', function (d) {
|
||||
return height - yScale(d.y);
|
||||
});
|
||||
|
||||
return bars;
|
||||
};
|
||||
|
|
|
@ -11,8 +11,9 @@ define(function (require) {
|
|||
*/
|
||||
return {
|
||||
histogram: Private(require('components/vislib/visualizations/column_chart')),
|
||||
pie: Private(require('components/vislib/visualizations/pie_chart')),
|
||||
line: Private(require('components/vislib/visualizations/line_chart'))
|
||||
line: Private(require('components/vislib/visualizations/line_chart')),
|
||||
area: Private(require('components/vislib/visualizations/area_chart')),
|
||||
pie: Private(require('components/vislib/visualizations/pie_chart'))
|
||||
};
|
||||
};
|
||||
|
||||
|
|
50
src/kibana/plugins/vis_types/area.js
Normal file
50
src/kibana/plugins/vis_types/area.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
define(function (require) {
|
||||
return function HistogramVisType(Private) {
|
||||
var VisType = Private(require('plugins/vis_types/_vis_type'));
|
||||
var Schemas = Private(require('plugins/vis_types/_schemas'));
|
||||
|
||||
return new VisType({
|
||||
name: 'area',
|
||||
title: 'Area chart',
|
||||
icon: 'fa-area-chart',
|
||||
vislibParams: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
},
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
name: 'metric',
|
||||
title: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1,
|
||||
defaults: [
|
||||
{ schema: 'metric', type: 'count' }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
name: 'segment',
|
||||
title: 'X-Axis',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
name: 'group',
|
||||
title: 'Split Area',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
name: 'split',
|
||||
title: 'Split Chart',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
])
|
||||
});
|
||||
};
|
||||
});
|
|
@ -4,4 +4,5 @@ define(function (require) {
|
|||
visTypes.register(require('plugins/vis_types/vislib/histogram'));
|
||||
visTypes.register(require('plugins/vis_types/vislib/line'));
|
||||
visTypes.register(require('plugins/vis_types/vislib/pie'));
|
||||
visTypes.register(require('plugins/vis_types/area'));
|
||||
});
|
|
@ -2,7 +2,10 @@
|
|||
<tbody>
|
||||
<tr ng-repeat="detail in details" >
|
||||
<td><b>{{detail.label}}</b></td>
|
||||
<td>{{detail.value}}</td>
|
||||
<td>
|
||||
{{detail.value}}
|
||||
<span ng-if="detail.percent"> ({{detail.percent}})</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -97,6 +97,7 @@ define(function (require) {
|
|||
|
||||
var label;
|
||||
var val;
|
||||
var percent;
|
||||
|
||||
switch (col) {
|
||||
case colX:
|
||||
|
@ -105,7 +106,8 @@ define(function (require) {
|
|||
break;
|
||||
case colY:
|
||||
label = 'y';
|
||||
val = datum.y;
|
||||
val = datum.value;
|
||||
percent = datum.percent;
|
||||
break;
|
||||
case colColor:
|
||||
label = 'color';
|
||||
|
@ -118,7 +120,8 @@ define(function (require) {
|
|||
|
||||
return {
|
||||
label: label,
|
||||
value: val
|
||||
value: val,
|
||||
percent: percent
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -174,7 +174,6 @@ notifications {
|
|||
|
||||
@import "./_table.less";
|
||||
@import "./_notify.less";
|
||||
@import "./_typeahead.less";
|
||||
|
||||
//== Nav tweaks
|
||||
.nav-condensed > li > a {
|
||||
|
|
14
src/server/lib/FrameOptions.rb
Normal file
14
src/server/lib/FrameOptions.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This monkeypatch is needed to ensure the X-Frame-Options header is
|
||||
# never set by rack-protection.
|
||||
#
|
||||
# http://stackoverflow.com/a/19232793/296172
|
||||
#
|
||||
module Rack
|
||||
module Protection
|
||||
class FrameOptions < Base
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ require "lib/ColorLogger"
|
|||
require "routes/home"
|
||||
require "sinatra/json"
|
||||
require "routes/proxy"
|
||||
require "lib/FrameOptions"
|
||||
require "routes/plugins"
|
||||
|
||||
class Logger
|
||||
|
|
|
@ -148,6 +148,7 @@
|
|||
'specs/components/agg_response/hierarchical/_transform_aggregation',
|
||||
'specs/components/agg_response/hierarchical/_create_raw_data',
|
||||
'specs/components/agg_response/hierarchical/_array_to_linked_list',
|
||||
'specs/components/agg_response/hierarchical/_collect_branch',
|
||||
'specs/components/agg_response/tabify/tabify_agg_response'
|
||||
], function (kibana, sinon) {
|
||||
kibana.load(function () {
|
||||
|
|
56
test/unit/specs/visualize/_collect_branch.js
Normal file
56
test/unit/specs/visualize/_collect_branch.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
define(function (require) {
|
||||
var collectBranch = require('components/visualize/_collect_branch');
|
||||
describe('collectBranch()', function () {
|
||||
var results;
|
||||
var convert = function (name) {
|
||||
return 'converted:' + name;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
results = collectBranch({
|
||||
name: 'bucket3',
|
||||
depth: 3,
|
||||
value: 6,
|
||||
field: { format: { convert: convert } },
|
||||
aggConfig: { params: { field: { name: 'field3' } } },
|
||||
parent: {
|
||||
name: 'bucket2',
|
||||
depth: 2,
|
||||
value: 12,
|
||||
aggConfig: { label: 'field2' },
|
||||
parent: {
|
||||
name: 'bucket1',
|
||||
depth: 1,
|
||||
value: 24,
|
||||
parent: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array with bucket objects', function () {
|
||||
expect(results).to.be.an(Array);
|
||||
expect(results).to.have.length(3);
|
||||
|
||||
expect(results[0]).to.have.property('metric', 24);
|
||||
expect(results[0]).to.have.property('depth', 0);
|
||||
expect(results[0]).to.have.property('bucket', 'bucket1');
|
||||
expect(results[0]).to.have.property('field', 'level 1');
|
||||
expect(results[0]).to.have.property('aggConfig');
|
||||
|
||||
expect(results[1]).to.have.property('metric', 12);
|
||||
expect(results[1]).to.have.property('depth', 1);
|
||||
expect(results[1]).to.have.property('bucket', 'bucket2');
|
||||
expect(results[1]).to.have.property('field', 'field2');
|
||||
expect(results[1]).to.have.property('aggConfig');
|
||||
|
||||
expect(results[2]).to.have.property('metric', 6);
|
||||
expect(results[2]).to.have.property('depth', 2);
|
||||
expect(results[2]).to.have.property('bucket', 'bucket3');
|
||||
expect(results[2]).to.have.property('field', 'field3');
|
||||
expect(results[2]).to.have.property('aggConfig');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
define(function (require) {
|
||||
console.log('loading other plugin');
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue