mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
f90fef6541
commit
00c3e1f216
44 changed files with 4262 additions and 177 deletions
|
@ -121,3 +121,5 @@ include::visualize/tilemap.asciidoc[]
|
|||
include::visualize/vertbar.asciidoc[]
|
||||
|
||||
include::visualize/tagcloud.asciidoc[]
|
||||
|
||||
include::visualize/heatmap.asciidoc[]
|
||||
|
|
83
docs/visualize/heatmap.asciidoc
Normal file
83
docs/visualize/heatmap.asciidoc
Normal 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[]
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
99
src/core_plugins/kbn_vislib_vis_types/public/heatmap.js
Normal file
99
src/core_plugins/kbn_vislib_vis_types/public/heatmap.js
Normal 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'
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,6 +12,7 @@ uiModules
|
|||
scope: {
|
||||
vis: '=',
|
||||
savedVis: '=',
|
||||
uiState: '=',
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
const $optionContainer = $el.find('.visualization-options');
|
||||
|
|
|
@ -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;
|
||||
}));
|
||||
|
|
|
@ -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";
|
||||
|
|
62
src/ui/public/vislib/__tests__/components/heatmap_color.js
Normal file
62
src/ui/public/vislib/__tests__/components/heatmap_color.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
|
|
157
src/ui/public/vislib/__tests__/visualizations/heatmap_chart.js
Normal file
157
src/ui/public/vislib/__tests__/visualizations/heatmap_chart.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
2565
src/ui/public/vislib/components/color/colormaps.js
Normal file
2565
src/ui/public/vislib/components/color/colormaps.js
Normal file
File diff suppressed because it is too large
Load diff
69
src/ui/public/vislib/components/color/heatmap_color.js
Normal file
69
src/ui/public/vislib/components/color/heatmap_color.js
Normal 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;
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.vis-alerts-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vis-alert {
|
||||
margin: 0 10px 10px;
|
||||
padding: 5px 10px 5px 5px;
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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++;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
122
test/functional/apps/visualize/_heatmap_chart.js
Normal file
122
test/functional/apps/visualize/_heatmap_chart.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -40,4 +40,5 @@ bdd.describe('visualize app', function () {
|
|||
require('./_pie_chart');
|
||||
require('./_tile_map');
|
||||
require('./_vertical_bar_chart');
|
||||
require('./_heatmap_chart');
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue