Vislib heatmap visualization (#9641)

Backports PR #9403

**Commit 1:**
adding UI styles (should extract)

* Original sha: 8815e1c1ce
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T09:56:56Z

**Commit 2:**
adding heatmap vislib type

* Original sha: d3b3065603
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T10:04:27Z

**Commit 3:**
adding heatmap visualization

* Original sha: 9bc3380648
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T10:05:34Z

**Commit 4:**
adding documentation

* Original sha: 8c888d4b25
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T20:24:09Z

**Commit 5:**
renaming heatmap options

* Original sha: 55a8b5f87f
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T20:24:57Z

**Commit 6:**
fixing options issues

* Original sha: f98a4559af
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T20:26:51Z

**Commit 7:**
fixing color selection

* Original sha: 44fe11a218
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T20:30:59Z

**Commit 8:**
fixing / adding tests

* Original sha: 91d921d3d8
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-07T21:52:52Z

**Commit 9:**
adding more color schemas

* Original sha: 6e80819140
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-08T09:59:41Z

**Commit 10:**
adding more options

* Original sha: 56569c4db6
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-08T10:00:10Z

**Commit 11:**
adding cell labels

* Original sha: 98dbfac2b6
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-08T12:45:27Z

**Commit 12:**
fixing selenium test

* Original sha: 9b0f4aa37e
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-08T13:05:49Z

**Commit 13:**
only allow to rotate labels by 90 degrees

* Original sha: 26bd2e97a5
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-09T18:11:02Z

**Commit 14:**
converting color number slider to number input

* Original sha: 45ba2b95b7
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-09T19:19:59Z

**Commit 15:**
hide labels if they don't fit

* Original sha: a1553bc7cf
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-09T19:20:19Z

**Commit 16:**
fixing small issues

* Original sha: 2867c2d8c2
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-09T19:37:20Z

**Commit 17:**
improved range options

* Original sha: 4ce88b086a
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-09T20:31:15Z

**Commit 18:**
fixing based on Thomas' review

* Original sha: 5b1951fa2d
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-14T08:14:34Z

**Commit 19:**
rebasing on master and fixing linting issues

* Original sha: 3399de000c
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-14T08:34:50Z

**Commit 20:**
adding selenium tests

* Original sha: c6b3e767c5
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-14T13:57:34Z

**Commit 21:**
fixing alerts

* Original sha: 1e195f7c0f
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-16T13:48:12Z

**Commit 22:**
fixing padding

* Original sha: 7d8718beab
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-16T13:48:30Z

**Commit 23:**
fixing based on review

* Original sha: d55b440174
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-16T13:49:25Z

**Commit 24:**
fixing math

* Original sha: 3fbb3f7480
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-16T14:25:33Z

**Commit 25:**
fixing custom range options

* Original sha: 644453de19
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-16T14:32:04Z

**Commit 26:**
removing $timeout

* Original sha: 016ab7ef0d
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-19T09:49:27Z

**Commit 27:**
notification in case labels were hidden

* Original sha: dae92bc933
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-19T10:06:54Z

**Commit 28:**
fixing tests

* Original sha: 8653ea112b
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-19T10:43:21Z

**Commit 29:**
fixing based on last review

* Original sha: 92ad40750d
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-19T14:26:41Z

**Commit 30:**
fixing based on thomas' review

* Original sha: c72ce4bdfd
* Authored by ppisljar <peter.pisljar@gmail.com> on 2016-12-26T17:26:48Z
This commit is contained in:
jasper 2016-12-26 15:27:40 -05:00 committed by Peter Pisljar
parent f90fef6541
commit 00c3e1f216
44 changed files with 4262 additions and 177 deletions

View file

@ -121,3 +121,5 @@ include::visualize/tilemap.asciidoc[]
include::visualize/vertbar.asciidoc[]
include::visualize/tagcloud.asciidoc[]
include::visualize/heatmap.asciidoc[]

View file

@ -0,0 +1,83 @@
[[heatmap-chart]]
== Heatmap Chart
A heat map is a graphical representation of data where the individual values contained in a matrix are represented as colors.
The color for each matrix position is determined by the _metrics_ aggregation. The following aggregations are available for
this chart:
include::y-axis-aggs.asciidoc[]
The _buckets_ aggregations determine what information is being retrieved from your data set.
Before you choose a buckets aggregation, specify if you are defining buckets for X or Y axis within a single chart
or splitting into multiple charts. A multiple chart split must run before any other aggregations.
When you split a chart, you can change if the splits are displayed in a row or a column by clicking
the *Rows | Columns* selector.
This chart's X and Y axis supports the following aggregations. Click the linked name of each aggregation to visit the main
Elasticsearch documentation for that aggregation.
*Date Histogram*:: A {es-ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a
numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days,
weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and
specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes,
*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision,
down to one second.
*Histogram*:: A standard {es-ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a
numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty
intervals in the histogram.
*Range*:: With a {es-ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges
of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove
a range.
*Date Range*:: A {es-ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values
that are within a range of dates that you specify. You can specify the ranges for the dates using
{es-ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints.
Click the red *(x)* symbol to remove a range.
*IPv4 Range*:: The {es-ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to
specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to
remove a range.
*Terms*:: A {es-ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top
or bottom _n_ elements of a given field to display, ordered by count or a custom metric.
*Filters*:: You can specify a set of {es-ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data.
You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to
add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where
you can type in a name to display on the visualization.
*Significant Terms*:: Displays the results of the experimental
{es-ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation.
Enter a string in the *Custom Label* field to change the display label.
You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation:
*Exclude Pattern*:: Specify a pattern in this field to exclude from the results.
*Include Pattern*:: Specify a pattern in this field to include in the results.
*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation
definition, as in the following example:
[source,shell]
{ "script" : "doc['grade'].value * 1.2" }
The availability of these options varies depending on the aggregation you choose.
Select the *Options* tab to change the following aspects of the chart:
*Show Tooltips*:: Check this box to enable the display of tooltips.
*Highlight*:: Check this box to enable highlighting of elements with same label
*Legend Position*:: You can select where to display the legend (top, left, right, bottom)
*Color Schema*:: You can select an existing color schema or go for custom and define your own colors in the legend
*Reverse Color Schema*:: Checking this checkbox will reverse the color schema.
*Color Scale*:: You can switch between linear, log and sqrt scales for color scale.
*Scale to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check
this box to change both upper and lower bounds to match the values returned in the data.
*Number of Colors*:: Number of color buckets to create. Minimum is 2 and maximum is 10.
*Percentage Mode*:: Enabling this will show legend values as percentages.
*Custom Range*:: You can define custom ranges for your color buckets. For each of the color bucket you need to specify
the minimum value (inclusive) and the maximum value (exclusive) of a range.
*Show Label*:: Enables showing labels with cell values in each cell
*Rotate*:: Allows rotating the cell value label by 90 degrees.
include::visualization-raw-data.asciidoc[]

View file

@ -0,0 +1,206 @@
<div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="colorSchema">
Color Schema
</label>
<div class="kuiSideBarFormRow__control">
<select
id="colorSchema"
class="kuiSelect kuiSideBarSelect"
ng-model="vis.params.colorSchema"
ng-options="mode for mode in vis.type.params.colorSchemas"
></select>
</div>
<div class="text-info text-center" ng-show="customColors" ng-click="resetColors()">reset colors</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="invertColors">
Reverse Color Schema
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="invertColors" type="checkbox" ng-model="vis.params.invertColors">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="axisScale">
Color Scale
</label>
<div class="kuiSideBarFormRow__control">
<select
id="axisScale"
class="kuiSelect kuiSideBarSelect"
ng-model="valueAxis.scale.type"
ng-options="mode for mode in vis.type.params.scales"
></select>
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="defaultYExtents">
Scale to Data Bounds
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="defaultYExtents" type="checkbox" ng-model="valueAxis.scale.defaultYExtents">
</div>
</div>
<div class="kuiSideBarFormRow" ng-if="!vis.params.setColorRange">
<label class="kuiSideBarFormRow__label" for="percentageMode">
Percentage Mode
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="percentageMode" type="checkbox" ng-model="vis.params.percentageMode">
</div>
</div>
<div class="kuiSideBarFormRow" ng-if="!vis.params.setColorRange">
<label class="kuiSideBarFormRow__label" for="colorsNumber">
Number of colors
</label>
<div class="kuiSideBarFormRow__control">
<input
id="colorsNumber"
class="kuiInput kuiSideBarInput"
ng-model="vis.params.colorsNumber"
type="number"
greater-than="1"
less-than="11"
>
</div>
</div>
<div>
<div class="kuiSideBarCollapsibleTitle">
<div
class="kuiSideBarCollapsibleTitle__label"
ng-click="toggleColorRangeSection()"
>
<span
aria-hidden="true"
ng-class="{ 'fa-caret-down': showColorRange, 'fa-caret-right': !showColorRange }"
class="fa fa-caret-right kuiSideBarCollapsibleTitle__caret"
></span>
<span class="kuiSideBarCollapsibleTitle__text">
Custom Ranges
</span>
</div>
<input aria-label="enable"
ng-model="vis.params.setColorRange"
type="checkbox"
class="kuiSideBarSectionTitle__action"
ng-click="toggleColorRangeSection(true)"
>
</div>
<div ng-if="vis.params.setColorRange" ng-show="showColorRange" class="kuiSideBarCollapsibleSection">
<div class="kuiSideBarSection">
<table class="vis-editor-agg-editor-ranges form-group" ng-show="vis.params.colorsRange.length">
<tr>
<th>
<label>From</label>
</th>
<th colspan="2">
<label>To</label>
</th>
</tr>
<tr ng-repeat="range in vis.params.colorsRange track by $index">
<td>
<input
ng-model="range.from"
type="number"
class="form-control"
name="range.from"
greater-or-equal-than="{{getGreaterThan($index)}}"
step="any" />
</td>
<td>
<input
ng-model="range.to"
type="number"
class="form-control"
name="range.to"
greater-than="range.from"
step="any" />
</td>
<td>
<button
type="button"
ng-click="removeRange($index)"
class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</td>
</tr>
</table>
<div class="hintbox" ng-show="!vis.params.colorsRange.length">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You must specify at least one range.
</p>
</div>
<div
ng-click="addRange()"
class="sidebar-item-button primary">
Add Range
</div>
<div class="text text-center text-info">Note: colors can be changed in the legend</div>
</div>
</div>
</div>
<div>
<div class="kuiSideBarCollapsibleTitle">
<div
class="kuiSideBarCollapsibleTitle__label"
ng-click="toggleLabelSection()"
>
<span
aria-hidden="true"
ng-class="{
'fa-caret-down': showLabels,
'fa-caret-right': !showLabels
}"
class="fa fa-caret-right kuiSideBarCollapsibleTitle__caret"
></span>
<span class="kuiSideBarCollapsibleTitle__text">
Show Labels
</span>
</div>
<input aria-label="enable"
ng-model="valueAxis.labels.show"
type="checkbox"
class="kuiSideBarSectionTitle__action"
>
</div>
<div ng-if="valueAxis.labels.show" ng-show="showLabels" class="kuiSideBarCollapsibleSection">
<div class="kuiSideBarSection">
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="rotateLabels">
Rotate
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="rotateLabels" type="checkbox" ng-model="options.rotateLabels">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="labelColor">
Color
</label>
<div class="kuiSideBarFormRow__control">
<input
id="labelColor"
class="kuiInput kuiSideBarInput"
ng-model="valueAxis.labels.color"
>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
import uiModules from 'ui/modules';
import heatmapOptionsTemplate from 'plugins/kbn_vislib_vis_types/controls/heatmap_options.html';
import _ from 'lodash';
const module = uiModules.get('kibana');
module.directive('heatmapOptions', function () {
return {
restrict: 'E',
template: heatmapOptionsTemplate,
replace: true,
link: function ($scope) {
$scope.showColorRange = false;
$scope.showLabels = false;
$scope.customColors = false;
$scope.options = {
rotateLabels: false
};
$scope.valueAxis = $scope.vis.params.valueAxes[0];
$scope.$watch('options.rotateLabels', rotate => {
$scope.vis.params.valueAxes[0].labels.rotate = rotate ? 270 : 0;
});
$scope.resetColors = function () {
$scope.uiState.set('vis.colors', null);
$scope.customColors = false;
};
$scope.toggleColorRangeSection = function (checkbox = false) {
$scope.showColorRange = !$scope.showColorRange;
if (checkbox && !$scope.vis.params.setColorRange) $scope.showColorRange = false;
if (!checkbox && $scope.showColorRange && !$scope.vis.params.setColorRange) $scope.vis.params.setColorRange = true;
};
$scope.toggleLabelSection = function (checkbox = false) {
$scope.showLabels = !$scope.showLabels;
if (checkbox && !$scope.valueAxis.labels.show) $scope.showLabels = false;
if ($scope.showLabels && !$scope.valueAxis.labels.show) $scope.valueAxis.labels.show = true;
};
$scope.getGreaterThan = function (index) {
if (index === 0) return;
return $scope.vis.params.colorsRange[index - 1].to;
};
$scope.addRange = function () {
const previousRange = _.last($scope.vis.params.colorsRange);
const from = previousRange ? previousRange.to : 0;
$scope.vis.params.colorsRange.push({ from: from, to: null });
};
$scope.removeRange = function (index) {
$scope.vis.params.colorsRange.splice(index, 1);
};
$scope.getColor = function (index) {
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return colors ? Object.values(colors)[index] : 'transparent';
};
$scope.uiState.on('colorChanged', () => {
$scope.customColors = true;
});
}
};
});

View file

@ -0,0 +1,47 @@
<div class="kuiSideBarSection">
<div class="kuiSideBarSectionTitle">
<div class="kuiSideBarSectionTitle__text">
Basic Settings
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="addTooltip">
Show Tooltips
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="addTooltip" type="checkbox" ng-model="vis.params.addTooltip">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="enableHover">
Highlight
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="enableHover" type="checkbox" ng-model="vis.params.enableHover">
</div>
</div>
<div class="kuiSideBarFormRow" ng-show="vis.params.addLegend">
<label class="kuiSideBarFormRow__label" for="legendPosition">
Legend Position
</label>
<div class="kuiSideBarFormRow__control">
<select
id="legendPosition"
class="kuiSelect kuiSideBarSelect"
ng-model="vis.params.legendPosition"
ng-options="position.value as position.text for position in vis.type.params.legendPositions"
></select>
</div>
</div>
<div class="kuiSideBarSectionTitle">
<div class="kuiSideBarSectionTitle__text">
Heatmap Settings
</div>
</div>
<heatmap-options></heatmap-options>
</div>

View file

@ -0,0 +1,99 @@
import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type';
import VisSchemasProvider from 'ui/vis/schemas';
import heatmapTemplate from 'plugins/kbn_vislib_vis_types/editors/heatmap.html';
import heatmapColors from 'ui/vislib/components/color/colormaps';
export default function HeatmapVisType(Private) {
const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider);
const Schemas = Private(VisSchemasProvider);
return new VislibVisType({
name: 'heatmap',
title: 'Heatmap chart',
icon: 'fa-barcode',
description: 'A heat map is a graphical representation of data' +
' where the individual values contained in a matrix are represented as colors. ',
params: {
defaults: {
addTooltip: true,
addLegend: true,
enableHover: false,
legendPosition: 'right',
times: [],
colorsNumber: 4,
colorSchema: 'Greens',
setColorRange: false,
colorsRange: [],
invertColors: false,
percentageMode: false,
valueAxes: [{
show: false,
id: 'ValueAxis-1',
type: 'value',
scale: {
type: 'linear',
defaultYExtents: false,
},
labels: {
show: false,
rotate: 0,
color: '#555'
}
}]
},
legendPositions: [{
value: 'left',
text: 'left',
}, {
value: 'right',
text: 'right',
}, {
value: 'top',
text: 'top',
}, {
value: 'bottom',
text: 'bottom',
}],
scales: ['linear', 'log', 'square root'],
colorSchemas: Object.keys(heatmapColors),
editor: heatmapTemplate
},
schemas: new Schemas([
{
group: 'metrics',
name: 'metric',
title: 'Value',
min: 1,
max: 1,
aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev'],
defaults: [
{ schema: 'metric', type: 'count' }
]
},
{
group: 'buckets',
name: 'segment',
title: 'X-Axis',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
},
{
group: 'buckets',
name: 'group',
title: 'Y-Axis',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
},
{
group: 'buckets',
name: 'split',
title: 'Split Chart',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
}
])
});
}

View file

@ -4,3 +4,4 @@ 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/tile_map'));
visTypes.register(require('plugins/kbn_vislib_vis_types/heatmap'));

View file

@ -88,7 +88,7 @@
<div class="vis-editor-config" ng-show="sidebar.section == 'options'">
<!-- vis options -->
<vis-editor-vis-options vis="vis" saved-vis="savedVis"></vis-editor-vis-options>
<vis-editor-vis-options vis="vis" saved-vis="savedVis" ui-state="uiState"></vis-editor-vis-options>
</div>

View file

@ -15,6 +15,7 @@ uiModules
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$bind('vis', 'editableVis');
$scope.$watch('vis.type', (visType) => {
if (visType) {
this.showData = visType.schemas.buckets || visType.schemas.metrics;

View file

@ -1,6 +1,3 @@
<div class="sidebar-item" ng-show="vis.type.params.editor">
<div class="sidebar-item-title">
view options
</div>
<div class="visualization-options"></div>
</div>

View file

@ -12,6 +12,7 @@ uiModules
scope: {
vis: '=',
savedVis: '=',
uiState: '=',
},
link: function ($scope, $el) {
const $optionContainer = $el.find('.visualization-options');

View file

@ -8,6 +8,7 @@ function makeDirectiveDef(id, compare) {
let getBound = function () { return $parse($attr[id])(); };
let defaultVal = {
'greaterThan': -Infinity,
'greaterOrEqualThan': -Infinity,
'lessThan': Infinity
}[id];
@ -36,4 +37,7 @@ uiModules
}))
.directive('lessThan', makeDirectiveDef('lessThan', function (a, b) {
return a < b;
}))
.directive('greaterOrEqualThan', makeDirectiveDef('greaterOrEqualThan', function (a, b) {
return a >= b;
}));

View file

@ -1036,4 +1036,225 @@ fieldset {
}
}
// TODO: Extract these styles into the UI Framework.
.kuiFormSection {
margin-bottom: 16px;
}
.kuiFormLabel {
display: block;
}
.kuiFormSubLabel {
display: block;
font-weight: normal;
}
.kuiTextArea,
.kuiInput,
.kuiStaticInput {
display: block;
width: 100%;
font-size: 14px;
color: #2d2d2d;
border: 1px solid;
border-radius: 4px;
}
.kuiStaticInput {
padding: 5px 0;
border-color: transparent;
}
.kuiInput,
.kuiTextArea {
padding: 5px 15px;
border-color: #D4D4D4;
}
.kuiSelect {
display: block;
width: 100%;
height: 32px;
padding: 5px 15px;
font-size: 14px;
line-height: 1.42857143;
color: #2D2D2D;
background-color: #ffffff;
background-image: none;
border: 1px solid #D4D4D4;
border-radius: 4px;
}
/**
* 1. Override Bootstrap checkbox styles.
*/
.kuiCheckBox {
margin: 0 !important; // 1
}
.kuiSideBarSelect {
// TODO: @include kuiSelect styles when this is moved to the UI Framework and we're using SASS.
height: 24px;
font-size: 12px;
padding: 0 15px;
}
.kuiSideBarInput {
// TODO: @include kuiInput styles when this is moved to the UI Framework and we're using SASS.
height: 24px;
font-size: 12px;
}
.kuiSideBarSection {
margin-bottom: 6px;
}
.kuiSideBarSectionTitle {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
border-bottom: 1px solid #D4D4D4;
}
.kuiSideBarSectionTitle__text {
font-size: 14px;
font-weight: 700;
color: #5A5A5A;
}
/**
* 1. Override Bootstrap button styles.
*/
.kuiSideBarSectionTitle__action {
background-color: transparent; // 1
padding: 0; // 1
cursor: pointer;
color: #328CAA;
&:hover {
color: #105A73;
}
}
.kuiSideBarCollapsibleTitle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 4px;
border-bottom: 1px solid #E8E8E8;
}
.kuiSideBarCollapsibleTitle__label {
display: flex;
align-items: center;
}
/**
* 1. Override FontAwesome .fa styles.
*/
.kuiSideBarCollapsibleTitle__caret {
width: 12px;
height: 12px;
font-size: 12px !important; // 1
font-weight: 700;
color: #5A5A5A;
}
.kuiSideBarCollapsibleTitle__text {
font-size: 12px;
font-weight: 700;
color: #5A5A5A;
}
/**
* 1. Override Bootstrap button styles.
*/
.kuiSideBarCollapsibleTitle__action {
background-color: transparent; // 1
padding: 0; // 1
cursor: pointer;
color: #328CAA;
&:hover {
color: #105A73;
}
}
.kuiSideBarCollapsibleSection {
padding-left: 16px;
}
/**
* 1. Override Bootstrap h1 styles.
*/
.kuiSideBarFormSectionTitle {
font-size: 12px;
font-weight: 700;
color: #5A5A5A;
border-bottom: 1px solid #E8E8E8;
margin: 4px 0 !important; // 1
padding-bottom: 2px;
}
.kuiSideBarFormRow {
display: flex;
align-items: center;
min-height: 24px;
margin-bottom: 1px;
}
/**
* 1. Override .vis-editor-sidebar styles.
*/
.kuiSideBarFormRow__label {
display: flex;
align-items: center;
flex: 1 1 40% !important; // 1
font-weight: 400;
}
.kuiSideBarFormRow__label__colorbox {
width: 24px;
height: 24px;
display: inline-block;
margin-right: 10px;
border: 1px ridge lightgray;
}
.kuiSideBarFormRow__control {
display: flex;
align-items: center;
flex: 1 1 60%;
}
/**
* 1. Override .sidebar-item styles.
*/
.kuiSideBarOptionsLink {
display: flex;
align-items: baseline;
margin-bottom: 8px;
font-size: 12px;
color: #328CAA !important; // 1
&:hover {
color: #105A73 !important; // 1
}
}
.kuiSideBarOptionsLink__caret {
width: 10px;
height: 10px;
}
.kuiSideBarOptionsLink__text {
margin-left: 2px;
}
@import "~dragula/dist/dragula.css";

View file

@ -0,0 +1,62 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import getColors from 'ui/vislib/components/color/heatmap_color';
describe('Vislib Heatmap Color Module Test Suite', function () {
const emptyObject = {};
const nullValue = null;
let notAValue;
beforeEach(ngMock.module('kibana'));
it('should throw an error if input is not a number', function () {
expect(function () {
getColors([200]);
}).to.throwError();
expect(function () {
getColors('help');
}).to.throwError();
expect(function () {
getColors(true);
}).to.throwError();
expect(function () {
getColors(notAValue);
}).to.throwError();
expect(function () {
getColors(nullValue);
}).to.throwError();
expect(function () {
getColors(emptyObject);
}).to.throwError();
});
it('should throw an error if input is less than 0', function () {
expect(function () {
getColors(-2);
}).to.throwError();
});
it('should throw an error if input is greater than 9', function () {
expect(function () {
getColors(10);
}).to.throwError();
});
it('should be a function', function () {
expect(typeof getColors).to.be('function');
});
it('should return a color for numbers from 0 to 9', function () {
const colorRegex = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/;
const schema = 'Greens';
for (let i = 0; i < 10; i++) {
expect(getColors(i / 10, schema)).to.match(colorRegex);
}
});
});

View file

@ -108,9 +108,8 @@ describe('Vislib AxisTitle Class Test Suite', function () {
dataObj = new Data(data, new PersistedState());
const visConfig = new VisConfig({
type: 'histogram',
el: el.node()
}, data, new PersistedState());
type: 'histogram'
}, data, new PersistedState(), el.node());
const xAxisConfig = new AxisConfig(visConfig, {
position: 'bottom',
title: {

View file

@ -96,9 +96,8 @@ describe('Vislib ChartTitle Class Test Suite', function () {
type: 'histogram',
title: {
'text': 'rows'
},
el: el.node()
}, data, persistedState);
}
}, data, persistedState, el.node());
chartTitle = new ChartTitle(visConfig);
}));

View file

@ -71,10 +71,9 @@ dateHistogramArray.forEach(function (data, i) {
describe('layout Method', function () {
beforeEach(function () {
let visConfig = new VisConfig({
el: vis.el,
const visConfig = new VisConfig({
type: 'histogram'
}, data, persistedState);
}, data, persistedState, vis.el);
testLayout = new Layout(visConfig);
});

View file

@ -76,9 +76,8 @@ describe('Vislib VisConfig Class Test Suite', function () {
.node();
visConfig = new VisConfig({
type: 'point_series',
el: el
}, data, new PersistedState());
type: 'point_series'
}, data, new PersistedState(), el);
}));
describe('get Method', function () {

View file

@ -94,10 +94,9 @@ describe('Vislib xAxis Class Test Suite', function () {
fixture = el.append('div')
.attr('class', 'x-axis-div');
let visConfig = new VisConfig({
el: $('.x-axis-div')[0],
const visConfig = new VisConfig({
type: 'histogram'
}, data, persistedState);
}, data, persistedState, $('.x-axis-div')[0]);
xAxis = new Axis(visConfig, {
type: 'category',
id: 'CategoryAxis-1'

View file

@ -75,10 +75,9 @@ function createData(seriesData) {
.attr('class', 'y-axis-div');
buildYAxis = function (params) {
let visConfig = new VisConfig({
el: node,
const visConfig = new VisConfig({
type: 'histogram'
}, data, persistedState);
}, data, persistedState, node);
return new YAxis(visConfig, _.merge({}, {
id: 'ValueAxis-1',
type: 'value',

View file

@ -0,0 +1,157 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import _ from 'lodash';
import d3 from 'd3';
// Data
import series from 'fixtures/vislib/mock_data/date_histogram/_series';
import seriesPosNeg from 'fixtures/vislib/mock_data/date_histogram/_series_pos_neg';
import seriesNeg from 'fixtures/vislib/mock_data/date_histogram/_series_neg';
import termsColumns from 'fixtures/vislib/mock_data/terms/_columns';
import stackedSeries from 'fixtures/vislib/mock_data/date_histogram/_stacked_series';
import $ from 'jquery';
import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture';
import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state';
// tuple, with the format [description, mode, data]
const dataTypesArray = [
['series', series],
['series with positive and negative values', seriesPosNeg],
['series with negative values', seriesNeg],
['terms columns', termsColumns],
['stackedSeries', stackedSeries],
];
describe('Vislib Heatmap Chart Test Suite', function () {
dataTypesArray.forEach(function (dataType, i) {
const name = dataType[0];
const data = dataType[1];
describe('for ' + name + ' Data', function () {
let PersistedState;
let vislibVis;
let vis;
let persistedState;
const visLibParams = {
type: 'heatmap',
addLegend: true,
addTooltip: true,
colorsNumber: 4,
colorSchema: 'Greens',
setColorRange: false,
percentageMode: true,
invertColors: false,
colorsRange: []
};
function generateVis(opts = {}) {
const config = _.defaultsDeep({}, opts, visLibParams);
vis = vislibVis(config);
persistedState = new PersistedState();
vis.on('brush', _.noop);
vis.render(data, persistedState);
}
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
vislibVis = Private(FixturesVislibVisFixtureProvider);
PersistedState = Private(PersistedStatePersistedStateProvider);
generateVis();
}));
afterEach(function () {
vis.destroy();
});
describe('addSquares method', function () {
it('should append rects', function () {
let numOfSeries;
let numOfValues;
let product;
vis.handler.charts.forEach(function (chart) {
const numOfRects = chart.chartData.series.reduce((result, series) => {
return result + series.values.length;
}, 0);
expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects);
});
});
});
describe('addBarEvents method', function () {
function checkChart(chart) {
const rect = $(chart.chartEl).find('.series rect').get(0);
return {
click: !!rect.__onclick,
mouseOver: !!rect.__onmouseover,
// D3 brushing requires that a g element is appended that
// listens for mousedown events. This g element includes
// listeners, however, I was not able to test for the listener
// function being present. I will need to update this test
// in the future.
brush: !!d3.select('.brush')[0][0]
};
}
it('should attach the brush if data is a set of ordered dates', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const date = Boolean(ordered && ordered.date);
expect(has.brush).to.be(date);
});
});
it('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
expect(has.click).to.be(true);
});
});
it('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
expect(has.mouseOver).to.be(true);
});
});
});
describe('draw method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.draw())).to.be(true);
});
});
it('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
expect(domain[0]).to.not.be(undefined);
expect(domain[1]).to.not.be(undefined);
});
});
});
it('should define default colors', function () {
expect(persistedState.get('vis.defaultColors')).to.not.be(undefined);
});
it('should set custom range', function () {
vis.destroy();
generateVis({
setColorRange: true,
colorsRange: [{ from: 0, to: 200 }, { from: 200, to: 400 }, { from: 400, to: 500 }, { from: 500, to: Infinity }]
});
const labels = vis.getLegendLabels();
expect(labels[0]).to.be('0 - 200');
expect(labels[1]).to.be('200 - 400');
expect(labels[2]).to.be('400 - 500');
expect(labels[3]).to.be('500 - Infinity');
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,69 @@
import _ from 'lodash';
import colormaps from './colormaps';
function enforceBounds(x) {
if (x < 0) {
return 0;
} else if (x > 1) {
return 1;
} else {
return x;
}
}
function interpolateLinearly(x, values) {
// Split values into four lists
const xValues = [];
const rValues = [];
const gValues = [];
const bValues = [];
values.forEach(value => {
xValues.push(value[0]);
rValues.push(value[1][0]);
gValues.push(value[1][1]);
bValues.push(value[1][2]);
});
let i = 1;
while (xValues[i] < x) i++;
const width = Math.abs(xValues[i - 1] - xValues[i]);
const scalingFactor = (x - xValues[i - 1]) / width;
// Get the new color values though interpolation
const r = rValues[i - 1] + scalingFactor * (rValues[i] - rValues[i - 1]);
const g = gValues[i - 1] + scalingFactor * (gValues[i] - gValues[i - 1]);
const b = bValues[i - 1] + scalingFactor * (bValues[i] - bValues[i - 1]);
return [enforceBounds(r), enforceBounds(g), enforceBounds(b)];
}
function getColor(value, colorSchemaName) {
if (!_.isNumber(value) || value < 0 || value > 1) {
throw new Error('heatmap_color expects a number from 0 to 1 as first parameter');
}
const colorSchema = colormaps[colorSchemaName];
if (!colorSchema) {
throw new Error('invalid colorSchemaName provided');
}
const color = interpolateLinearly(value, colorSchema);
const r = Math.round(255 * color[0]);
const g = Math.round(255 * color[1]);
const b = Math.round(255 * color[2]);
return `rgb(${r},${g},${b})`;
}
function drawColormap(colorSchema, width = 100, height = 10) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
for (let i = 0; i <= width; i++) {
ctx.fillStyle = getColor(i / width, colorSchema);
ctx.fillRect(i, 0, 1, height);
}
return canvas;
}
getColor.prototype.drawColormap = drawColormap;
export default getColor;

View file

@ -11,87 +11,66 @@ export default function AlertsFactory(Private) {
* @param el {HTMLElement} Reference to DOM element
*/
class Alerts {
constructor(vis, data, alertDefs) {
constructor(vis, alertDefs) {
this.vis = vis;
this.data = data;
this.binder = new Binder();
this.alertDefs = alertDefs || [];
this.data = vis.data;
this.alertDefs = _.cloneDeep(alertDefs);
this.binder.jqOn(vis.el, 'mouseenter', '.vis-alerts-tray', function () {
const $tray = $(this);
hide();
$(vis.el).on('mousemove', checkForExit);
function hide() {
$tray.css({
'pointer-events': 'none',
opacity: 0.3
});
}
function show() {
$(vis.el).off('mousemove', checkForExit);
$tray.css({
'pointer-events': 'auto',
opacity: 1
});
}
function checkForExit(event) {
const pos = $tray.offset();
if (pos.top > event.clientY || pos.left > event.clientX) return show();
const bottom = pos.top + $tray.height();
if (event.clientY > bottom) return show();
const right = pos.left + $tray.width();
if (event.clientX > right) return show();
}
});
this.alerts = _(alertDefs)
.map(alertDef => {
if (!alertDef) return;
if (alertDef.test && !alertDef.test(vis, this.data)) return;
return this._addAlert(alertDef);
})
.compact();
}
/**
* Renders chart titles
*
* @method render
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
*/
_addAlert(alertDef) {
const type = alertDef.type || 'info';
const icon = alertDef.icon || type;
const msg = alertDef.msg;
// alert container
const $icon = $('<i>').addClass('vis-alerts-icon fa fa-' + icon);
const $text = $('<p>').addClass('vis-alerts-text').text(msg);
const $closeIcon = $('<i>').addClass('fa fa-close');
const $closeDiv = $('<div>').addClass('vis-alerts-close').append($closeIcon);
const $alert = $('<div>').addClass('vis-alert vis-alert-' + type).append([$icon, $text, $closeDiv]);
$closeDiv.on('click', e => {
$alert.remove();
});
return $alert;
}
// renders initial alerts
render() {
const alerts = this.alerts;
const vis = this.vis;
const data = this.data;
const alerts = _(this.alertDefs)
.map(function (alertDef) {
if (!alertDef) return;
if (alertDef.test && !alertDef.test(vis, data)) return;
const type = alertDef.type || 'info';
const icon = alertDef.icon || type;
const msg = alertDef.msg;
// alert container
const $icon = $('<i>').addClass('vis-alerts-icon fa fa-' + icon);
const $text = $('<p>').addClass('vis-alerts-text').text(msg);
return $('<div>').addClass('vis-alert vis-alert-' + type).append([$icon, $text]);
})
.compact();
$(vis.el).find('.vis-alerts').append($('<div>').addClass('vis-alerts-tray'));
if (!alerts.size()) return;
$(vis.el).find('.vis-alerts-tray').append(alerts.value());
}
$(vis.el).find('.vis-alerts').append(
$('<div>').addClass('vis-alerts-tray').append(alerts.value())
// shows new alert
show(msg, type) {
const vis = this.vis;
const alert = {
msg: msg,
type: type
};
if (this.alertDefs.find(alertDef => alertDef.msg === alert.msg)) return;
this.alertDefs.push(alert);
$(vis.el).find('.vis-alerts-tray').append(
this._addAlert(alert)
);
};
}
/**
* Tear down the Alerts
* @return {undefined}
*/
destroy() {
this.binder.destroy();
};
$(this.vis.el).find('.vis-alerts').remove();
}
}
return Alerts;
};
}

View file

@ -277,7 +277,7 @@ export default function AxisFactory(Private) {
return function (selection) {
const n = selection[0].length;
if (self.axisTitle) {
if (config.get('show') && self.axisTitle) {
self.axisTitle.render(selection);
}
selection.each(function () {

View file

@ -25,7 +25,9 @@ export default function AxisConfigFactory() {
opacity: 1,
tickColor: '#ddd',
tickWidth: '1px',
tickLength: '6px'
tickLength: '6px',
rangePadding: 0.1,
rangeOuterPadding: 0
},
labels: {
axisFormatter: null,
@ -47,6 +49,15 @@ export default function AxisConfigFactory() {
const categoryDefaults = {
type: 'category',
position: 'bottom',
};
const valueDefaults = {
labels: {
axisFormatter: d3.format('n')
}
};
const horizontalDefaults = {
labels: {
rotate: 0,
rotateAnchor: 'end',
@ -55,9 +66,9 @@ export default function AxisConfigFactory() {
}
};
const valueDefaults = {
const verticalDefaults = {
labels: {
axisFormatter: d3.format('n')
rotateAnchor: 'middle'
}
};
@ -67,6 +78,7 @@ export default function AxisConfigFactory() {
// _.defaultsDeep mutates axisConfigArgs nested values so we clone it first
const axisConfigArgsClone = _.cloneDeep(axisConfigArgs);
this._values = _.defaultsDeep({}, axisConfigArgsClone, typeDefaults, defaults);
_.merge(this._values, this.isHorizontal() ? horizontalDefaults : verticalDefaults);
this._values.elSelector = this._values.elSelector.replace('{pos}', this._values.position);
this._values.rootEl = chartConfig.get('el');
@ -114,6 +126,7 @@ export default function AxisConfigFactory() {
if (this.isHorizontal() && this.isOrdinal()) {
this._values.labels.filter = _.get(axisConfigArgs, 'labels.filter', false);
this._values.labels.rotate = _.get(axisConfigArgs, 'labels.rotate', 90);
this._values.labels.truncate = _.get(axisConfigArgs, 'labels.truncate', 100);
}
let offset;

View file

@ -130,8 +130,8 @@ export default function AxisScaleFactory(Private) {
const max = this.axisConfig.get('scale.max') || this.getYMax();
const domain = [min, max];
if (this.axisConfig.isUserDefined()) return this.validateUserExtents(domain);
if (this.axisConfig.isYExtents()) return domain;
if (this.axisConfig.isLogScale()) return this.logDomain(min, max);
if (this.axisConfig.isYExtents()) return domain;
return [Math.min(0, min), Math.max(0, max)];
};
@ -179,16 +179,18 @@ export default function AxisScaleFactory(Private) {
const scale = this.getD3Scale(config.getScaleType());
const domain = this.getExtents();
const range = this.getRange(length);
const padding = config.get('style.rangePadding');
const outerPadding = config.get('style.rangeOuterPadding');
this.scale = scale.domain(domain);
if (config.isOrdinal()) {
this.scale.rangeBands(range, 0.1);
this.scale.rangeBands(range, padding, outerPadding);
} else {
this.scale.range(range);
}
if (this.canApplyNice()) this.scale.nice();
// Prevents bars from going off the chart when the y extents are within the domain range
if (this.visConfig.get('type') === 'histogram' && this.scale.clamp) this.scale.clamp(true);
if (this.scale.clamp) this.scale.clamp(true);
this.validateScale(this.scale);

View file

@ -46,6 +46,7 @@ export default function DataFactory(Private) {
newVal.aggConfig = val.aggConfig;
newVal.aggConfigResult = val.aggConfigResult;
newVal.extraMetrics = val.extraMetrics;
newVal.series = val.series || seri.label;
return newVal;
})
};
@ -74,7 +75,7 @@ export default function DataFactory(Private) {
_getLabels(data) {
return this.type === 'series' ? getLabels(data) : this.pieNames();
};
}
getDataType() {
const data = this.getVisData();
@ -91,7 +92,7 @@ export default function DataFactory(Private) {
});
return type;
};
}
/**
* Returns an array of the actual x and y data value objects
@ -106,15 +107,16 @@ export default function DataFactory(Private) {
return _.toArray(arr);
}
return [this.data];
};
}
shouldBeStacked(seriesConfig) {
if (!seriesConfig) return false;
const isHistogram = (seriesConfig.type === 'histogram');
const isArea = (seriesConfig.type === 'area');
const stacked = (seriesConfig.mode === 'stacked');
return (isHistogram || isArea) && stacked;
};
}
getStackedSeries(chartConfig, axis, series, first = false) {
const matchingSeries = [];
@ -125,7 +127,7 @@ export default function DataFactory(Private) {
}
});
return matchingSeries;
};
}
stackChartData(handler, data, chartConfig) {
const stackedData = {};
@ -136,7 +138,7 @@ export default function DataFactory(Private) {
axis.stack(_.map(stackedData[id], 'values'));
});
return stackedData;
};
}
stackData(handler) {
const data = this.data;
@ -168,7 +170,7 @@ export default function DataFactory(Private) {
}
return visData;
};
}
/**
* get min and max for all cols, rows of data
@ -184,8 +186,8 @@ export default function DataFactory(Private) {
min: Math.min(props.min, minMax.min),
max: Math.max(props.max, minMax.max)
};
}, {min: Infinity, max: -Infinity});
};
}, { min: Infinity, max: -Infinity });
}
/**
* Returns array of chart data objects for pie data objects
@ -198,7 +200,7 @@ export default function DataFactory(Private) {
return this.data.rows ? this.data.rows : this.data.columns;
}
return [this.data];
};
}
/**
* Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter`
@ -213,7 +215,7 @@ export default function DataFactory(Private) {
get(thing, def) {
const source = (this.data.rows || this.data.columns || [this.data])[0];
return _.get(source, thing, def);
};
}
/**
* Returns true if null values are present
@ -229,7 +231,7 @@ export default function DataFactory(Private) {
});
});
});
};
}
/**
* Return an array of all value objects
@ -246,7 +248,7 @@ export default function DataFactory(Private) {
.pluck('values')
.flattenDeep()
.value();
};
}
/**
* Validates that the Y axis min value defined by user input
@ -260,7 +262,7 @@ export default function DataFactory(Private) {
throw new Error('validateUserDefinedYMin expects a number');
}
return val;
};
}
/**
* Helper function for getNames
@ -294,7 +296,7 @@ export default function DataFactory(Private) {
});
return names;
};
}
/**
* Flattens hierarchical data into an array of objects with a name and index value.
@ -321,7 +323,7 @@ export default function DataFactory(Private) {
})
.value();
}
};
}
/**
* Removes zeros from pie chart data
@ -342,7 +344,7 @@ export default function DataFactory(Private) {
}, []);
return slices;
};
}
/**
* Returns an array of names ordered by appearance in the nested array
@ -365,7 +367,7 @@ export default function DataFactory(Private) {
});
return _.uniq(names, 'label');
};
}
/**
* Inject zeros into the data
@ -375,7 +377,7 @@ export default function DataFactory(Private) {
*/
injectZeros(data, orderBucketsBySum = false) {
return injectZeros(data, this.data, orderBucketsBySum);
};
}
/**
* Returns an array of all x axis values from the data
@ -385,7 +387,7 @@ export default function DataFactory(Private) {
*/
xValues(orderBucketsBySum = false) {
return orderKeys(this.data, orderBucketsBySum);
};
}
/**
* Return an array of unique labels
@ -397,7 +399,7 @@ export default function DataFactory(Private) {
*/
getLabels() {
return getLabels(this.data);
};
}
/**
* Returns a function that does color lookup on labels
@ -406,8 +408,11 @@ export default function DataFactory(Private) {
* @returns {Function} Performs lookup on string and returns hex color
*/
getColorFunc() {
return color(this.getLabels(), this.uiState.get('vis.colors'));
};
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return color(this.getLabels(), colors);
}
/**
* Returns a function that does color lookup on names for pie charts
@ -419,7 +424,7 @@ export default function DataFactory(Private) {
return color(this.pieNames(this.getVisData()).map(function (d) {
return d.label;
}), this.uiState.get('vis.colors'));
};
}
/**
* ensure that the datas ordered property has a min and max
@ -443,7 +448,7 @@ export default function DataFactory(Private) {
if (missingMax) d.ordered.max = extent[1];
}
});
};
}
/**
* Calculates min and max values for all map data
@ -456,12 +461,11 @@ export default function DataFactory(Private) {
* @returns {Array} min and max values
*/
mapDataExtents(series) {
let values;
values = _.map(series.rows, function (row) {
const values = _.map(series.rows, function (row) {
return row[row.length - 1];
});
return [_.min(values), _.max(values)];
};
}
/**
* Get the maximum number of series, considering each chart
@ -473,8 +477,8 @@ export default function DataFactory(Private) {
return this.chartData().reduce(function (max, chart) {
return Math.max(max, chart.series.length);
}, 0);
};
}
}
return Data;
};
}

View file

@ -14,6 +14,7 @@ export default function TypeFactory(Private) {
line: pointSeries.line,
pie: Private(VislibLibTypesPieProvider),
area: pointSeries.area,
point_series: pointSeries.line
point_series: pointSeries.line,
heatmap: pointSeries.heatmap,
};
};

View file

@ -1,4 +1,5 @@
import _ from 'lodash';
import errors from 'ui/errors';
export default function ColumnHandler(Private) {
@ -44,7 +45,8 @@ export default function ColumnHandler(Private) {
return function (cfg, data) {
const isUserDefinedYAxis = cfg.setYExtents;
const config = _.defaults({}, cfg, {
const config = _.cloneDeep(cfg);
_.defaultsDeep(config, {
chartTitle: {},
mode: 'normal'
}, opts);
@ -104,6 +106,8 @@ export default function ColumnHandler(Private) {
config.charts = createCharts(cfg, data.data);
}
if (typeof config.enableHover === 'undefined') config.enableHover = true;
return config;
};
}
@ -140,6 +144,32 @@ export default function ColumnHandler(Private) {
}
}
]
})
}),
heatmap: (cfg, data) => {
const defaults = create()(cfg, data);
defaults.valueAxes[0].show = false;
defaults.categoryAxes[0].style = {
rangePadding: 0,
rangeOuterPadding: 0
};
defaults.valueAxes.push({
id: 'CategoryAxis-2',
type: 'category',
position: 'left',
values: data.getLabels(),
scale: {
inverted: true
},
labels: {
axisFormatter: val => val
},
style: {
rangePadding: 0,
rangeOuterPadding: 0
}
});
return defaults;
}
};
};

View file

@ -13,20 +13,21 @@ export default function VisConfigFactory(Private) {
style: {
margin : { top: 10, right: 3, bottom: 5, left: 3 }
},
alerts: {},
alerts: [],
categoryAxes: [],
valueAxes: []
};
class VisConfig {
constructor(visConfigArgs, data, uiState) {
constructor(visConfigArgs, data, uiState, el) {
this.data = new Data(data, uiState);
const visType = visTypes[visConfigArgs.type];
const typeDefaults = visType(visConfigArgs, this.data);
this._values = _.defaultsDeep({}, typeDefaults, DEFAULT_VIS_CONFIG);
};
this._values.el = el;
}
get(property, defaults) {
if (_.has(this._values, property) || typeof defaults !== 'undefined') {
@ -35,11 +36,11 @@ export default function VisConfigFactory(Private) {
throw new Error(`Accessing invalid config property: ${property}`);
return defaults;
}
};
}
set(property, value) {
return _.set(this._values, property, value);
};
}
}
return VisConfig;

View file

@ -30,6 +30,10 @@
padding: 0;
}
.vis-alerts-close {
cursor: pointer;
}
.vis-alert {
margin: 0 10px 10px;
padding: 5px 10px 5px 5px;

View file

@ -27,8 +27,7 @@ export default function VisFactory(Private) {
super(arguments);
this.el = $el.get ? $el.get(0) : $el;
this.binder = new Binder();
this.visConfigArgs = visConfigArgs;
this.visConfigArgs.el = this.el;
this.visConfigArgs = _.cloneDeep(visConfigArgs);
// bind the resize function so it can be used as an event handler
this.resize = _.bind(this.resize, this);
@ -67,11 +66,20 @@ export default function VisFactory(Private) {
uiState.on('change', this._uiStateChangeHandler);
}
this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState);
this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState, this.el);
this.handler = new Handler(this, this.visConfig);
this._runWithoutResizeChecker('render');
};
getLegendLabels() {
return this.visConfig ? this.visConfig.get('legend.labels', null) : null;
}
getLegendColors() {
return this.visConfig ? this.visConfig.get('legend.colors', null) : null;
}
/**
* Resizes the visualization
*

View file

@ -33,6 +33,11 @@ export default function PointSeriesFactory(Private) {
this.chartEl = chartEl;
this.chartConfig = this.findChartConfig();
this.handler.pointSeries = this;
const seriesLimit = 25;
if (this.chartConfig.series.length > seriesLimit) {
throw new errors.VislibError('There are too many series defined.');
}
}
findChartConfig() {

View file

@ -19,13 +19,13 @@ export default function PointSeriesProvider(Private) {
if (this.getValueAxis().axisConfig.isLogScale() && invalidLogScale) {
throw new errors.InvalidLogScaleValues();
}
};
}
getStackedCount() {
return this.baseChart.chartConfig.series.reduce(function (sum, series) {
return series.mode === 'stacked' ? sum + 1 : sum;
}, 0);
};
}
getGroupedCount() {
const stacks = [];
@ -38,7 +38,7 @@ export default function PointSeriesProvider(Private) {
if (isStacked) stacks.push(valueAxis);
return sum + 1;
}, 0);
};
}
getStackedNum(data) {
let i = 0;
@ -47,7 +47,7 @@ export default function PointSeriesProvider(Private) {
if (seri.mode === 'stacked') i++;
}
return 0;
};
}
getGroupedNum(data) {
let i = 0;
@ -64,27 +64,30 @@ export default function PointSeriesProvider(Private) {
}
}
return 0;
};
}
getValueAxis() {
return _.find(this.handler.valueAxes, axis => {
return axis.axisConfig.get('id') === this.seriesConfig.valueAxis;
}) || this.handler.valueAxes[0];
};
}
getCategoryAxis() {
return _.find(this.handler.categoryAxes, axis => {
return axis.axisConfig.get('id') === this.seriesConfig.categoryAxis;
}) || this.handler.categoryAxes[0];
};
}
addCircleEvents(element) {
const events = this.events;
const hover = events.addHoverEvent();
const mouseout = events.addMouseoutEvent();
if (this.handler.visConfig.get('enableHover')) {
const hover = events.addHoverEvent();
const mouseout = events.addMouseoutEvent();
element.call(hover).call(mouseout);
}
const click = events.addClickEvent();
return element.call(hover).call(mouseout).call(click);
};
return element.call(click);
}
checkIfEnoughData() {
const message = 'Area charts require more than one data point. Try adding ' +
@ -95,8 +98,8 @@ export default function PointSeriesProvider(Private) {
if (notEnoughData) {
throw new errors.NotEnoughData(message);
}
};
}
}
return PointSeries;
};
}

View file

@ -0,0 +1,279 @@
import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
import moment from 'moment';
import VislibVisualizationsPointSeriesProvider from './_point_series';
import getColor from 'ui/vislib/components/color/heatmap_color';
export default function HeatmapChartFactory(Private) {
const PointSeries = Private(VislibVisualizationsPointSeriesProvider);
const defaults = {
color: undefined, // todo
fillColor: undefined // todo
};
/**
* Line Chart Visualization
*
* @class HeatmapChart
* @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 HeatmapChart extends PointSeries {
constructor(handler, chartEl, chartData, seriesConfigArgs) {
super(handler, chartEl, chartData, seriesConfigArgs);
this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults);
this.handler.visConfig.set('legend', {
labels: this.getHeatmapLabels(this.handler.visConfig),
colors: this.getHeatmapColors(this.handler.visConfig)
});
const colors = this.handler.visConfig.get('legend.colors', null);
if (colors) {
this.handler.vis.uiState.setSilent('vis.defaultColors', null);
this.handler.vis.uiState.setSilent('vis.defaultColors', colors);
}
}
getHeatmapLabels(cfg) {
const percentageMode = cfg.get('percentageMode');
const colorsNumber = cfg.get('colorsNumber');
const colorsRange = cfg.get('colorsRange');
const zScale = this.getValueAxis().getScale();
const [min, max] = zScale.domain();
const labels = [];
if (cfg.get('setColorRange')) {
colorsRange.forEach(range => {
const from = range.from;
const to = range.to;
labels.push(`${from} - ${to}`);
});
} else {
for (let i = 0; i < colorsNumber; i++) {
let label;
let val = i / colorsNumber;
let nextVal = (i + 1) / colorsNumber;
if (percentageMode) {
val = Math.ceil(val * 100);
nextVal = Math.ceil(nextVal * 100);
label = `${val}% - ${nextVal}%`;
} else {
val = val * (max - min) + min;
nextVal = nextVal * (max - min) + min;
if (max > 1) {
val = Math.ceil(val);
nextVal = Math.ceil(nextVal);
}
label = `${val} - ${nextVal}`;
}
labels.push(label);
}
}
return labels;
}
getHeatmapColors(cfg) {
const colorsNumber = cfg.get('colorsNumber');
const invertColors = cfg.get('invertColors');
const colorSchema = cfg.get('colorSchema');
const labels = this.getHeatmapLabels(cfg);
const colors = {};
for (const i in labels) {
if (labels[i]) {
const val = invertColors ? 1 - i / colorsNumber : i / colorsNumber;
colors[labels[i]] = getColor(val, colorSchema);
}
}
return colors;
}
addSquares(svg, data) {
const xScale = this.getCategoryAxis().getScale();
const yScale = this.handler.valueAxes[1].getScale();
const zScale = this.getValueAxis().getScale();
const ordered = this.handler.data.get('ordered');
const tooltip = this.baseChart.tooltip;
const isTooltip = this.handler.visConfig.get('tooltip.show');
const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal();
const colorsNumber = this.handler.visConfig.get('colorsNumber');
const setColorRange = this.handler.visConfig.get('setColorRange');
const colorsRange = this.handler.visConfig.get('colorsRange');
const color = this.handler.data.getColorFunc();
const labels = this.handler.visConfig.get('legend.labels');
const zAxisConfig = this.getValueAxis().axisConfig;
const zAxisFormatter = zAxisConfig.get('labels.axisFormatter');
const showLabels = zAxisConfig.get('labels.show');
const layer = svg.append('g')
.attr('class', 'series');
const squares = layer
.selectAll('g.square')
.data(data.values);
squares
.exit()
.remove();
let barWidth;
if (this.getCategoryAxis().axisConfig.isTimeDomain()) {
const { min, interval } = this.handler.data.get('ordered');
const start = min;
const end = moment(min).add(interval).valueOf();
barWidth = xScale(end) - xScale(start);
if (!isHorizontal) barWidth *= -1;
}
function x(d) {
return xScale(d.x);
}
function y(d) {
return yScale(d.series);
}
const [min, max] = zScale.domain();
function getColorBucket(d) {
let val = 0;
if (setColorRange && colorsRange.length) {
const bucket = _.find(colorsRange, range => {
return range.from <= d.y && range.to > d.y;
});
return bucket ? colorsRange.indexOf(bucket) : -1;
} else {
if (isNaN(min) || isNaN(max)) {
val = colorsNumber - 1;
} else {
val = (d.y - min) / (max - min); /* get val from 0 - 1 */
val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber));
}
}
return val;
}
function label(d) {
const colorBucket = getColorBucket(d);
if (colorBucket === -1) d.hide = true;
return labels[colorBucket];
}
function z(d) {
if (label(d) === '') return 'transparent';
return color(label(d));
}
const squareWidth = barWidth || xScale.rangeBand();
const squareHeight = yScale.rangeBand();
squares
.enter()
.append('g')
.attr('class', 'square');
squares.append('rect')
.attr('x', x)
.attr('width', squareWidth)
.attr('y', y)
.attr('height', squareHeight)
.attr('data-label', label)
.attr('fill', z)
.attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px')
.style('display', d => {
return d.hide ? 'none' : 'initial';
});
// todo: verify that longest label is not longer than the barwidth
// or barwidth is not smaller than textheight (and vice versa)
//
if (showLabels) {
const rotate = zAxisConfig.get('labels.rotate');
const rotateRad = rotate * Math.PI / 180;
const cellPadding = 5;
const maxLength = Math.min(
Math.abs(squareWidth / Math.cos(rotateRad)),
Math.abs(squareHeight / Math.sin(rotateRad))
) - cellPadding;
const maxHeight = Math.min(
Math.abs(squareWidth / Math.sin(rotateRad)),
Math.abs(squareHeight / Math.cos(rotateRad))
) - cellPadding;
let hiddenLabels = false;
squares.append('text')
.text(d => zAxisFormatter(d.y))
.style('display', function (d) {
const textLength = this.getBBox().width;
const textHeight = this.getBBox().height;
const textTooLong = textLength > maxLength;
const textTooWide = textHeight > maxHeight;
if (!d.hide && (textTooLong || textTooWide)) {
hiddenLabels = true;
}
return d.hide || textTooLong || textTooWide ? 'none' : 'initial';
})
.style('dominant-baseline', 'central')
.style('text-anchor', 'middle')
.style('fill', zAxisConfig.get('labels.color'))
.attr('x', function (d) {
const center = x(d) + squareWidth / 2;
return center;
})
.attr('y', function (d) {
const center = y(d) + squareHeight / 2;
return center;
})
.attr('transform', function (d) {
const horizontalCenter = x(d) + squareWidth / 2;
const verticalCenter = y(d) + squareHeight / 2;
return `rotate(${rotate},${horizontalCenter},${verticalCenter})`;
});
if (hiddenLabels) {
this.baseChart.handler.alerts.show('Some labels were hidden due to size constrains');
}
}
if (isTooltip) {
squares.call(tooltip.render());
}
return squares.selectAll('rect');
}
/**
* Renders d3 visualization
*
* @method draw
* @returns {Function} Creates the line chart
*/
draw() {
const self = this;
return function (selection) {
selection.each(function () {
const svg = self.chartEl.append('g');
svg.data([self.chartData]);
const squares = self.addSquares(svg, self.chartData);
self.addCircleEvents(squares);
self.events.emit('rendered', {
chart: self.chartData
});
return svg;
});
};
}
}
return HeatmapChart;
}

View file

@ -1,12 +1,14 @@
import VislibVisualizationsColumnChartProvider from './column_chart';
import VislibVisualizationsLineChartProvider from './line_chart';
import VislibVisualizationsAreaChartProvider from './area_chart';
import VislibVisualizationsHeatmapChartProvider from './heatmap_chart';
export default function SeriesTypeFactory(Private) {
return {
histogram: Private(VislibVisualizationsColumnChartProvider),
line: Private(VislibVisualizationsLineChartProvider),
area: Private(VislibVisualizationsAreaChartProvider)
area: Private(VislibVisualizationsAreaChartProvider),
heatmap: Private(VislibVisualizationsHeatmapChartProvider)
};
};

View file

@ -11,6 +11,7 @@ module.exports = function VislibRenderbotFactory(Private, $injector) {
_.class(VislibRenderbot).inherits(Renderbot);
function VislibRenderbot(vis, $el, uiState) {
VislibRenderbot.Super.call(this, vis, $el, uiState);
this.refreshLegend = 0;
this._createVis();
}
@ -26,6 +27,7 @@ module.exports = function VislibRenderbotFactory(Private, $injector) {
if (this.chartData) {
this.vislibVis.render(this.chartData, this.uiState);
this.refreshLegend++;
}
};
@ -49,6 +51,7 @@ module.exports = function VislibRenderbotFactory(Private, $injector) {
this.chartData = this.buildChartData(esResponse);
return AngularPromise.delay(1).then(() => {
this.vislibVis.render(this.chartData, this.uiState);
this.refreshLegend++;
});
};

View file

@ -3,6 +3,7 @@ import 'ui/vislib';
import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options';
import 'plugins/kbn_vislib_vis_types/controls/point_series_options';
import 'plugins/kbn_vislib_vis_types/controls/line_interpolation_option';
import 'plugins/kbn_vislib_vis_types/controls/heatmap_options';
import VisSchemasProvider from 'ui/vis/schemas';
import VisVisTypeProvider from 'ui/vis/vis_type';
import AggResponsePointSeriesPointSeriesProvider from 'ui/agg_response/point_series/point_series';

View file

@ -1,34 +1,35 @@
import _ from 'lodash';
import html from 'ui/visualize/visualize_legend.html';
import VislibLibDataProvider from 'ui/vislib/lib/data';
import VislibComponentsColorColorProvider from 'ui/vis/components/color/color';
import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler';
import uiModules from 'ui/modules';
uiModules.get('kibana')
.directive('visualizeLegend', function (Private, getAppState) {
let Data = Private(VislibLibDataProvider);
let colorPalette = Private(VislibComponentsColorColorProvider);
let filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider);
const Data = Private(VislibLibDataProvider);
const filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider);
return {
restrict: 'E',
template: html,
link: function ($scope) {
let $state = getAppState();
let clickHandler = filterBarClickHandler($state);
const $state = getAppState();
const clickHandler = filterBarClickHandler($state);
$scope.open = $scope.uiState.get('vis.legendOpen', true);
$scope.$watch('renderbot.chartData', function (data) {
if (!data) return;
$scope.data = data;
});
$scope.$watch('renderbot.refreshLegend', () => {
refresh();
});
$scope.highlight = function (event) {
let el = event.currentTarget;
let handler = $scope.renderbot.vislibVis.handler;
const el = event.currentTarget;
const handler = $scope.renderbot.vislibVis.handler;
//there is no guarantee that a Chart will set the highlight-function on its handler
if (!handler || typeof handler.highlight !== 'function') {
@ -38,8 +39,8 @@ uiModules.get('kibana')
};
$scope.unhighlight = function (event) {
let el = event.currentTarget;
let handler = $scope.renderbot.vislibVis.handler;
const el = event.currentTarget;
const handler = $scope.renderbot.vislibVis.handler;
//there is no guarantee that a Chart will set the unhighlight-function on its handler
if (!handler || typeof handler.unHighlight !== 'function') {
return;
@ -48,15 +49,18 @@ uiModules.get('kibana')
};
$scope.setColor = function (label, color) {
let colors = $scope.uiState.get('vis.colors') || {};
colors[label] = color;
const colors = $scope.uiState.get('vis.colors') || {};
if (colors[label] === color) delete colors[label];
else colors[label] = color;
$scope.uiState.setSilent('vis.colors', null);
$scope.uiState.set('vis.colors', colors);
$scope.uiState.emit('colorChanged');
refresh();
};
$scope.toggleLegend = function () {
let bwcAddLegend = $scope.vis.params.addLegend;
let bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend;
const bwcAddLegend = $scope.vis.params.addLegend;
const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend;
$scope.open = !$scope.uiState.get('vis.legendOpen', bwcLegendStateDefault);
$scope.uiState.set('vis.legendOpen', $scope.open);
};
@ -79,11 +83,11 @@ uiModules.get('kibana')
};
$scope.filter = function (legendData, negate) {
clickHandler({point: legendData, negate: negate});
clickHandler({ point: legendData, negate: negate });
};
$scope.canFilter = function (legendData) {
let filters = clickHandler({point: legendData}, true) || [];
const filters = clickHandler({ point: legendData }, true) || [];
return filters.length;
};
@ -98,14 +102,31 @@ uiModules.get('kibana')
];
function refresh() {
let vislibVis = $scope.renderbot.vislibVis;
if (!$scope.renderbot) return;
const vislibVis = $scope.renderbot.vislibVis;
if (!vislibVis.visConfig) {
$scope.labels = [{ label: 'loading ...' }];
return;
} // make sure vislib is defined at this point
if ($scope.uiState.get('vis.legendOpen') == null && $scope.vis.params.addLegend != null) {
$scope.open = $scope.vis.params.addLegend;
}
$scope.labels = getLabels($scope.data, vislibVis.visConfigArgs.type);
$scope.getColor = colorPalette(_.pluck($scope.labels, 'label'), $scope.uiState.get('vis.colors'));
if (vislibVis.visConfigArgs.type === 'heatmap') {
const labels = vislibVis.getLegendLabels();
if (labels) {
$scope.labels = _.map(labels, label => {
return { label: label };
});
}
} else {
$scope.labels = getLabels($scope.data, vislibVis.visConfigArgs.type);
}
if (vislibVis.visConfig) {
$scope.getColor = vislibVis.visConfig.data.getColorFunc();
}
}
// Most of these functions were moved directly from the old Legend class. Not a fan of this.
@ -117,7 +138,7 @@ uiModules.get('kibana')
}
function getSeriesLabels(data) {
let values = data.map(function (chart) {
const values = data.map(function (chart) {
return chart.series;
})
.reduce(function (a, b) {
@ -125,8 +146,6 @@ uiModules.get('kibana')
}, []);
return _.compact(_.uniq(values, 'label'));
}
}
};
});

View file

@ -15,8 +15,8 @@ bdd.describe('visualize app', function describeIndexTests() {
bdd.describe('chart types', function indexPatternCreation() {
bdd.it('should show the correct chart types', function () {
var expectedChartTypes = [
'Area chart', 'Data table', 'Line chart', 'Markdown widget',
const expectedChartTypes = [
'Area chart', 'Data table', 'Heatmap chart', 'Line chart', 'Markdown widget',
'Metric', 'Pie chart', 'Tag cloud', 'Tile map', 'Timeseries', 'Vertical bar chart'
];
// find all the chart types and make sure there all there

View file

@ -0,0 +1,122 @@
import expect from 'expect.js';
import {
bdd,
scenarioManager,
} from '../../../support';
import PageObjects from '../../../support/page_objects';
bdd.describe('visualize app', function describeIndexTests() {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
bdd.before(function () {
PageObjects.common.debug('navigateToApp visualize');
return PageObjects.common.navigateToApp('visualize')
.then(function () {
PageObjects.common.debug('clickHeatmapChart');
return PageObjects.visualize.clickHeatmapChart();
})
.then(function clickNewSearch() {
return PageObjects.visualize.clickNewSearch();
})
.then(function setAbsoluteRange() {
PageObjects.common.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
})
.then(function clickBucket() {
PageObjects.common.debug('Bucket = X-Axis');
return PageObjects.visualize.clickBucket('X-Axis');
})
.then(function selectAggregation() {
PageObjects.common.debug('Aggregation = Date Histogram');
return PageObjects.visualize.selectAggregation('Date Histogram');
})
.then(function selectField() {
PageObjects.common.debug('Field = @timestamp');
return PageObjects.visualize.selectField('@timestamp');
})
// leaving Interval set to Auto
.then(function clickGo() {
return PageObjects.visualize.clickGo();
})
.then(function () {
return PageObjects.header.isGlobalLoadingIndicatorHidden();
})
.then(function waitForVisualization() {
return PageObjects.visualize.waitForVisualization();
});
});
bdd.describe('heatmap chart', function indexPatternCreation() {
const vizName1 = 'Visualization HeatmapChart';
bdd.it('should save and load', function () {
return PageObjects.visualize.saveVisualization(vizName1)
.then(function (message) {
PageObjects.common.debug('Saved viz message = ' + message);
expect(message).to.be('Visualization Editor: Saved Visualization \"' + vizName1 + '\"');
})
.then(function testVisualizeWaitForToastMessageGone() {
return PageObjects.visualize.waitForToastMessageGone();
})
.then(function () {
return PageObjects.visualize.loadSavedVisualization(vizName1);
})
.then(function () {
return PageObjects.header.isGlobalLoadingIndicatorHidden();
})
.then(function waitForVisualization() {
return PageObjects.visualize.waitForVisualization();
});
});
bdd.it('should show correct chart, take screenshot', function () {
const expectedChartValues = ['0 - 400', '0 - 400', '400 - 800', '1200 - 1600',
'1200 - 1600', '400 - 800', '0 - 400', '0 - 400', '0 - 400', '0 - 400', '400 - 800',
'1200 - 1600', '1200 - 1600', '400 - 800', '0 - 400', '0 - 400', '0 - 400', '0 - 400',
'400 - 800', '1200 - 1600', '1200 - 1600', '400 - 800', '0 - 400', '0 - 400' ];
// Most recent failure on Jenkins usually indicates the bar chart is still being drawn?
// return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function
// try sleeping a bit before getting that data
return PageObjects.common.sleep(5000)
.then(function () {
return PageObjects.visualize.getHeatmapData();
})
.then(function showData(data) {
PageObjects.common.debug('data=' + data);
PageObjects.common.debug('data.length=' + data.length);
PageObjects.common.saveScreenshot('Visualize-heatmap-chart');
expect(data).to.eql(expectedChartValues);
});
});
bdd.it('should show correct data', function () {
// this is only the first page of the tabular data.
const expectedChartData = [ 'September 20th 2015, 00:00:00.000 37',
'September 20th 2015, 03:00:00.000 202',
'September 20th 2015, 06:00:00.000 740',
'September 20th 2015, 09:00:00.000 1,437',
'September 20th 2015, 12:00:00.000 1,371',
'September 20th 2015, 15:00:00.000 751',
'September 20th 2015, 18:00:00.000 188',
'September 20th 2015, 21:00:00.000 31',
'September 21st 2015, 00:00:00.000 42',
'September 21st 2015, 03:00:00.000 202'
];
return PageObjects.visualize.collapseChart()
.then(function showData(data) {
return PageObjects.visualize.getDataTableData();
})
.then(function showData(data) {
PageObjects.common.debug(data.split('\n'));
expect(data.trim().split('\n')).to.eql(expectedChartData);
});
});
});
});

View file

@ -40,4 +40,5 @@ bdd.describe('visualize app', function () {
require('./_pie_chart');
require('./_tile_map');
require('./_vertical_bar_chart');
require('./_heatmap_chart');
});

View file

@ -67,6 +67,13 @@ export default class VisualizePage {
.click();
}
clickHeatmapChart() {
return this.remote
.setFindTimeout(defaultFindTimeout)
.findByPartialLinkText('Heatmap chart')
.click();
}
getChartTypeCount() {
return this.remote
.setFindTimeout(defaultFindTimeout)
@ -606,6 +613,29 @@ export default class VisualizePage {
});
}
getHeatmapData() {
const self = this.remote;
// 1). get the maximim chart Y-Axis marker value
return this.remote
.setFindTimeout(defaultFindTimeout * 2)
// #kibana-body > div.content > div > div > div > div.vis-editor-canvas > visualize > div.visualize-chart > div > div.vis-col-wrapper > div.chart-wrapper > div > svg > g > g.series.\30 > rect:nth-child(1)
.findAllByCssSelector('svg > g > g.series rect') // rect
.then(function (chartTypes) {
PageObjects.common.debug('rects=' + chartTypes);
function getChartType(chart) {
return chart
.getAttribute('data-label');
}
const getChartTypesPromises = chartTypes.map(getChartType);
return Promise.all(getChartTypesPromises);
})
.then(function (labels) {
PageObjects.common.debug('labels=' + labels);
return labels;
});
}
getPieChartData() {
// 1). get the maximim chart Y-Axis marker value
return this.remote