From 4c85fd4954b2f7bdb9a02e2d1cbc3e3b752f7444 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 15 Feb 2017 17:54:42 +0100 Subject: [PATCH] Vislib Point Series Charts (#9642) * adding vislib chart grid * updating vislib to correctly render all new features * adding new options to kibana visualizations * update vis icon on save * updating documentation * fixing unit tests * cleaning up * updating based on UI review * adding visualize editor unit tests * selenium tests * additional option tabs * some more changes to the tabs/options [thanks CJ] * going back from Category/Value axis to X/Y axis * fixing unselected dropdown options * updating based on last UI review * updating based on last review * updating based on last review * fixing issue with axis titles * allowing to specify only upper or only lower data bound * updating based on brandons review * fixing horizontal bar chart labels * fixing test * adding backward compatibility * updating based on last review * fixing selenium tests --- docs/visualize.asciidoc | 11 +- docs/visualize/area.asciidoc | 76 ----- docs/visualize/line.asciidoc | 69 ----- docs/visualize/vertbar.asciidoc | 78 ------ docs/visualize/xychart.asciidoc | 126 +++++++++ .../kbn_vislib_vis_types/public/area.js | 117 ++++++-- .../controls/point_series/category_axis.html | 113 ++++++++ .../controls/point_series/category_axis.js | 32 +++ .../public/controls/point_series/grid.html | 48 ++++ .../public/controls/point_series/grid.js | 15 + .../public/controls/point_series/index.js | 4 + .../public/controls/point_series/series.html | 118 ++++++++ .../public/controls/point_series/series.js | 83 ++++++ .../controls/point_series/value_axes.html | 261 ++++++++++++++++++ .../controls/point_series/value_axes.js | 161 +++++++++++ .../public/editors/__tests__/point_series.js | 111 ++++++++ .../public/editors/area.html | 11 - .../public/editors/histogram.html | 13 - .../public/editors/line.html | 23 -- .../public/editors/point_series.html | 57 ++++ .../kbn_vislib_vis_types/public/histogram.js | 114 ++++++-- .../public/horizontal_bar.js | 157 +++++++++++ .../public/kbn_vislib_vis_types.js | 2 + .../kbn_vislib_vis_types/public/line.js | 86 ++++-- .../public/visualize/editor/editor.html | 2 +- .../kibana/public/visualize/editor/editor.js | 1 + .../public/visualize/editor/sidebar.html | 16 +- .../public/visualize/editor/vis_options.js | 3 +- .../point_series/__tests__/_add_to_siri.js | 8 +- .../point_series/__tests__/_get_series.js | 4 +- .../agg_response/point_series/_add_to_siri.js | 5 +- .../agg_response/point_series/_get_series.js | 4 +- src/ui/public/styles/base.less | 10 + src/ui/public/vis/__tests__/_vis.js | 1 - src/ui/public/vis/vis_type.js | 6 + .../public/vislib/__tests__/lib/axis/axis.js | 228 +++++++++++++++ src/ui/public/vislib/lib/axis/axis.js | 140 ++-------- src/ui/public/vislib/lib/axis/axis_config.js | 7 +- src/ui/public/vislib/lib/axis/axis_labels.js | 37 ++- src/ui/public/vislib/lib/axis/axis_title.js | 9 +- src/ui/public/vislib/lib/chart_grid.js | 79 ++++++ src/ui/public/vislib/lib/data.js | 9 +- src/ui/public/vislib/lib/handler.js | 3 + src/ui/public/vislib/lib/types/index.js | 1 + .../public/vislib/lib/types/point_series.js | 50 +++- src/ui/public/vislib/lib/vis_config.js | 3 +- src/ui/public/vislib/styles/_layout.less | 4 + src/ui/public/vislib/styles/_svg.less | 7 +- .../vislib/visualizations/point_series.js | 11 +- .../point_series/_point_series.js | 6 +- .../visualizations/point_series/area_chart.js | 5 +- .../point_series/column_chart.js | 8 +- .../visualizations/point_series/line_chart.js | 6 +- .../public/vislib_vis_type/vislib_vis_type.js | 40 +++ .../functional/apps/visualize/_chart_types.js | 2 +- .../apps/visualize/_metric_chart.js | 18 +- .../apps/visualize/_point_series_options.js | 192 +++++++++++++ .../apps/visualize/_vertical_bar_chart.js | 2 +- test/functional/apps/visualize/index.js | 1 + test/support/page_objects/index.js | 7 + test/support/page_objects/visualize_page.js | 30 +- .../visualize_point_series_options.js | 171 ++++++++++++ 62 files changed, 2461 insertions(+), 561 deletions(-) delete mode 100644 docs/visualize/area.asciidoc delete mode 100644 docs/visualize/line.asciidoc delete mode 100644 docs/visualize/vertbar.asciidoc create mode 100644 docs/visualize/xychart.asciidoc create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.js create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.js create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js delete mode 100644 src/core_plugins/kbn_vislib_vis_types/public/editors/area.html delete mode 100644 src/core_plugins/kbn_vislib_vis_types/public/editors/histogram.html delete mode 100644 src/core_plugins/kbn_vislib_vis_types/public/editors/line.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html create mode 100644 src/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js create mode 100644 src/ui/public/vislib/__tests__/lib/axis/axis.js create mode 100644 src/ui/public/vislib/lib/chart_grid.js create mode 100644 test/functional/apps/visualize/_point_series_options.js create mode 100644 test/support/page_objects/visualize_point_series_options.js diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index d624b252897a8..36c7b713356cb 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -25,10 +25,8 @@ To create a visualization: . Choose the visualization type: + [horizontal] -<>:: Visualize the total contribution of several -different series. +<>:: Compare different series in X/Y charts. <>:: Display the raw data of a composed aggregation. -<>:: Compare different series. <>:: Display free-form information or instructions. <>:: Display a single number. @@ -38,7 +36,6 @@ instructions. locations. Timeseries:: Compute and combine data from multiple time series data sets. -<>:: Graph values in a bar chart. . Specify a search query to retrieve the data for your visualization: ** To enter new search criteria, select the index pattern for the indices that @@ -104,12 +101,10 @@ For more information about working with sub aggregations, see https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[Kibana, Aggregation Execution Order, and You]. -include::visualize/area.asciidoc[] +include::visualize/xychart.asciidoc[] include::visualize/datatable.asciidoc[] -include::visualize/line.asciidoc[] - include::visualize/markdown.asciidoc[] include::visualize/metric.asciidoc[] @@ -118,8 +113,6 @@ include::visualize/pie.asciidoc[] include::visualize/tilemap.asciidoc[] -include::visualize/vertbar.asciidoc[] - include::visualize/tagcloud.asciidoc[] include::visualize/heatmap.asciidoc[] diff --git a/docs/visualize/area.asciidoc b/docs/visualize/area.asciidoc deleted file mode 100644 index a0124c67e4cd9..0000000000000 --- a/docs/visualize/area.asciidoc +++ /dev/null @@ -1,76 +0,0 @@ -[[area-chart]] -== Area Charts - -This chart's Y axis is the _metrics_ axis. The following aggregations are available for this axis: - -*Count*:: The {es-ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of -the elements in the selected index pattern. -*Average*:: This aggregation returns the {es-ref}search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric -field. Select a field from the drop-down. -*Sum*:: The {es-ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric -field. Select a field from the drop-down. -*Min*:: The {es-ref}search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a -numeric field. Select a field from the drop-down. -*Max*:: The {es-ref}search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a -numeric field. Select a field from the drop-down. -*Unique Count*:: The {es-ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns -the number of unique values in a field. Select a field from the drop-down. -*Percentiles*:: The {es-ref}search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a -percentile field. -*Percentile Rank*:: The {es-ref}search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field -from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a -values field. Click *+Add* to add a values field. - -You can add an aggregation by clicking the *+ Add Metrics* button. - -include::x-axis-aggs.asciidoc[] -For example, a chart of dates with incident counts can display dates in chronological order, or you can raise the -priority of the incident-reporting aggregation to show the most active dates first. The chronological order might show -a time-dependent pattern in incident count, and sorting by active dates can reveal particular outliers in your data. - -include::color-picker.asciidoc[] - -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" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{es-ref}modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* tab to change the following aspects of the chart: - -*Chart Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect -how the aggregations display on the chart: - -_stacked_:: Stacks the aggregations on top of each other. -_overlap_:: The aggregations overlap, with translucency indicating areas of overlap. -_wiggle_:: Displays the aggregations as a https://en.wikipedia.org/wiki/Streamgraph[streamgraph]. -_percentage_:: Displays each aggregation as a proportion of the total. -_silhouette_:: Displays each aggregation as variance from a central line. - -Checkboxes are available to enable and disable the following behaviors: - -*Line Mode*:: You can choose between straight line, smoothed line and stepped line. -*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific -values. -*Scale Y-Axis 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. -*Order buckets by descending sum*:: Check this box to enforce sorting of buckets by descending sum in the visualization -*Show Tooltip*:: Check this box to enable the display of tooltips. - -[float] -[[area-viewing-detailed-information]] -==== Viewing Detailed Information - -include::visualization-raw-data.asciidoc[] diff --git a/docs/visualize/line.asciidoc b/docs/visualize/line.asciidoc deleted file mode 100644 index 490b6bcb95d5f..0000000000000 --- a/docs/visualize/line.asciidoc +++ /dev/null @@ -1,69 +0,0 @@ -[[line-chart]] -== Line Charts - -This chart's Y axis is the _metrics_ axis. The following aggregations are available for this axis: - -include::y-axis-aggs.asciidoc[] - -Before you choose a buckets aggregation, specify if you are splitting slices 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. - -include::x-axis-aggs.asciidoc[] - -include::color-picker.asciidoc[] - -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. -*Exclude Pattern Flags*:: A standard set of Java flags for the exclusion pattern. -*Include Pattern*:: Specify a pattern in this field to include in the results. -*Include Pattern Flags*:: A standard set of Java flags for the inclusion pattern. -*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" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{es-ref}modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* tab to change the following aspects of the chart: - -*Y-Axis Scale*:: You can select *linear*, *log*, or *square root* scales for the chart's Y axis. You can use a log -scale to display data that varies exponentially, such as a compounding interest chart, or a square root scale to -regularize the display of data sets with variabilities that are themselves highly variable. This kind of data, where -the variability is itself variable over the domain being examined, is known as _heteroscedastic_ data. For example, if -a data set of height versus weight has a relatively narrow range of variability at the short end of height, but a wider -range at the taller end, the data set is heteroscedastic. -*Line Mode*:: You can choose between straight line, smoothed line and stepped line. -*Show Connecting Lines*:: Check this box to draw lines between the points on the chart. -*Show Circles*:: Check this box to draw each data point on the chart as a small circle. -*Current time marker*:: For charts of time-series data, check this box to draw a red line on the current time. -*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific -values. -*Show Tooltip*:: Check this box to enable the display of tooltips. -*Scale Y-Axis 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. -*Order buckets by descending sum*:: Check this box to enforce sorting of buckets by descending sum in the visualization - -After changing options, click the *Apply changes* button to update your visualization, or the grey *Discard -changes* button to keep your visualization in its current state. - -[float] -[[bubble-chart]] -==== Bubble Charts -You can convert a line chart visualization to a bubble chart by performing the following steps: - -. Click *Add Metrics* for the visualization's Y axis, then select *Dot Size*. -. Select a metric aggregation from the drop-down list. -. In the *Options* tab, uncheck the *Show Connecting Lines* box. -. Click the *Apply changes* button. - -[float] -[[line-viewing-detailed-information]] -==== Viewing Detailed Information - -include::visualization-raw-data.asciidoc[] diff --git a/docs/visualize/vertbar.asciidoc b/docs/visualize/vertbar.asciidoc deleted file mode 100644 index 35b0ea471a17a..0000000000000 --- a/docs/visualize/vertbar.asciidoc +++ /dev/null @@ -1,78 +0,0 @@ -[[vertical-bar-chart]] -== Vertical Bar Charts - -This chart's Y axis is the _metrics_ axis. The following aggregations are available for this axis: - -*Count*:: The {es-ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of -the elements in the selected index pattern. -*Average*:: This aggregation returns the {es-ref}search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric -field. Select a field from the drop-down. -*Sum*:: The {es-ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric -field. Select a field from the drop-down. -*Min*:: The {es-ref}search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a -numeric field. Select a field from the drop-down. -*Max*:: The {es-ref}search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a -numeric field. Select a field from the drop-down. -*Unique Count*:: The {es-ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns -the number of unique values in a field. Select a field from the drop-down. -*Percentiles*:: The {es-ref}search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the -values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one -or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a -percentile field. -*Percentile Rank*:: The {es-ref}search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] -aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field -from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a -values field. Click *+Add* to add a values field. - -You can add an aggregation by clicking the *+ Add Aggregation* button. - -Enter a string in the *Custom Label* field to change the display label. - -The _buckets_ aggregations determine what information is being retrieved from your data set. - -Before you choose a buckets aggregation, specify if you are splitting slices 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. - -include::x-axis-aggs.asciidoc[] - -include::color-picker.asciidoc[] - -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" } - -NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable -{es-ref}modules-scripting.html[dynamic Groovy scripting]. - -The availability of these options varies depending on the aggregation you choose. - -Select the *Options* to change the following aspects of the table: - -*Bar Mode*:: When you have multiple Y-axis aggregations defined for your chart, you can use this drop-down to affect -how the aggregations display on the chart: - -_stacked_:: Stacks the aggregations on top of each other. -_percentage_:: Displays each aggregation as a proportion of the total. -_grouped_:: Groups the results horizontally by the lowest-priority sub-aggregation. - -Checkboxes are available to enable and disable the following behaviors: - -*Show Tooltip*:: Check this box to enable the display of tooltips. -*Scale Y-Axis 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. -*Order buckets by descending sum*:: Check this box to enforce sorting of buckets by descending sum in the visualization - -[float] -[[vertbar-viewing-detailed-information]] -==== Viewing Detailed Information - -include::visualization-raw-data.asciidoc[] diff --git a/docs/visualize/xychart.asciidoc b/docs/visualize/xychart.asciidoc new file mode 100644 index 0000000000000..b33666b38210a --- /dev/null +++ b/docs/visualize/xychart.asciidoc @@ -0,0 +1,126 @@ +[[xy-chart]] +== X/Y Charts +X/Y charts refer to Area, Line and Bar charts which allow you to plot your data on X/Y axis. + +First you need to select your _metrics_ which define Value axis. The following aggregations are available for this axis: + +*Count*:: The {es-ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of +the elements in the selected index pattern. +*Average*:: This aggregation returns the {es-ref}search-aggregations-metrics-avg-aggregation.html[_average_] of a numeric +field. Select a field from the drop-down. +*Sum*:: The {es-ref}search-aggregations-metrics-sum-aggregation.html[_sum_] aggregation returns the total sum of a numeric +field. Select a field from the drop-down. +*Min*:: The {es-ref}search-aggregations-metrics-min-aggregation.html[_min_] aggregation returns the minimum value of a +numeric field. Select a field from the drop-down. +*Max*:: The {es-ref}search-aggregations-metrics-max-aggregation.html[_max_] aggregation returns the maximum value of a +numeric field. Select a field from the drop-down. +*Unique Count*:: The {es-ref}search-aggregations-metrics-cardinality-aggregation.html[_cardinality_] aggregation returns +the number of unique values in a field. Select a field from the drop-down. +*Percentiles*:: The {es-ref}search-aggregations-metrics-percentile-aggregation.html[_percentile_] aggregation divides the +values in a numeric field into percentile bands that you specify. Select a field from the drop-down, then specify one +or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a +percentile field. +*Percentile Rank*:: The {es-ref}search-aggregations-metrics-percentile-rank-aggregation.html[_percentile ranks_] +aggregation returns the percentile rankings for the values in the numeric field you specify. Select a numeric field +from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a +values field. Click *+Add* to add a values field. + +You can add an aggregation by clicking the *+ Add Aggregation* button. + +Enter a string in the *Custom Label* field to change the display label. + +The _buckets_ aggregations determine what information is being retrieved from your data set. + +Before you choose a buckets aggregation, specify if you are splitting slices 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. + +include::x-axis-aggs.asciidoc[] + +include::color-picker.asciidoc[] + +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" } + +NOTE: In Elasticsearch releases 1.4.3 and later, this functionality requires you to enable +{es-ref}modules-scripting.html[dynamic Groovy scripting]. + +The availability of these options varies depending on the aggregation you choose. + +=== Options + +Select the *Options* tab to change the way your data is visualized. Customization options are grouped into areas to provide easier access: + +==== General Settings + +*Legend Position*:: Allows you to move your legend to the *left*, *right*, *top* or *bottom* +*Show Tooltip*:: Enables or disables the display of tooltip on hovering over chart objects +*Order buckets by descending sum*:: Check this box to enforce sorting of buckets by descending sum in the visualization + +==== Category Axis +The category axis is defined by the bucket aggregation you chose under Data tab. Here you can customize how its displayed: + +*Show*:: You can chose if you want to display category axis or not +*Position*:: You can choose where you want to display category axis. If you position your category axis on the left or right the chart will turn to the horizontal type. +*Advanced Options*:: +*Labels - Show Labels*:::: Allows you to hide axis labels +*Labels - Filter Labels*:::: If filter labels is enabled some labels will be hidden in case there is not enough space to display them +*Labels - Rotate*:::: You can enter the number in degrees for how much you want to rotate labels +*Labels - Truncate*:::: You can enter the size in pixels to which the label is truncated + +==== Grid +You can enable grid on the chart. By default grid is displayed on the category axis only. + +*Category Lines*:: You can disable the display of grid lines on category axis +*Value Axis*:: You can choose on which (if any) of the value axes you want to display grid lines +*Color*:: You can specify the color for gird lines + +==== Value Axes +By default one value axis is defined on a chart, but you can add as much as you need. Clicking on the + sign will create a new value axis. + +Each value axis has this options: + +*Show*:: You can decide to hide the value axis completely +*Label*:: Allows to define a custom label +*Position*:: Options for position depend on the position of your category axis. If category axis is positioned on the top or bottom you can position value axis on the left or right. In the opposite case you can position your value axis on the top or bottom. +*Mode*:: Mode allows you to define how value axis represents the values. You can choose among the following: +_wiggle_:::: Displays the aggregations as a https://en.wikipedia.org/wiki/Streamgraph[streamgraph]. +_percentage_:::: Displays each aggregation as a proportion of the total. +_silhouette_:::: Displays each aggregation as variance from a central line. +*Scale Type*:: Allows you to choose between *linear*, *square root* and *log* scale + +*Advanced Options*:: +*Labels - Show Labels*:::: Allows you to hide axis labels +*Labels - Filter Labels*:::: If filter labels is enabled some labels will be hidden in case there is not enough spave to display them +*Labels - Rotate*:::: You can enter the number in degrees for how much you want to rotate labels +*Labels - Truncate*:::: You can enter the size in pixels to which the label is truncated +*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. +*Custom Extents*:::: You can define custom minimum and maximum for each axis + +==== Series +Each of the *Series* represents a metric you added in the data tab. For each Series you can define the following options: + +*Show*:: Allows you to hide specific series. +*Type*:: Allows you to choose between *Area*, *Line* and *Histogram* types. This allows you to show each metrics as a different chart type. +*Mode*:: Allows you to choose how your values are showed on the chart. +_stacked_:::: Values for this series will be stacked. Stacking happens per value axis. This means that if you have two series on one value axis and both modes are set to stacked they will be stacked on top of each other. If one of the series modes is set to normal the other series values (in case series are split) will be split and the second series will be grouped next to them. If you want both series to be stacked but not to be stacked on top of each other you will want to plot them on separate value axes. +_normal_:::: In normal mode values will not be stacked. +*Value Axis*:: You can define to which value axis this series belongs. If you dont select a value it will belong to the first value axis. + +Additional options might be available depending the on the *type* selected. For Area and Line types you can decide to smooth the lines. And for Line chart you can decide to not show lines or circles. + +[float] +[[vertbar-viewing-detailed-information]] +== Viewing Detailed Information + +include::visualization-raw-data.asciidoc[] diff --git a/src/core_plugins/kbn_vislib_vis_types/public/area.js b/src/core_plugins/kbn_vislib_vis_types/public/area.js index 15db616bf203b..bd2175ac09db4 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/area.js @@ -1,8 +1,8 @@ import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type'; import VisSchemasProvider from 'ui/vis/schemas'; -import areaTemplate from 'plugins/kbn_vislib_vis_types/editors/area.html'; +import pointSeriesTemplate from 'plugins/kbn_vislib_vis_types/editors/point_series.html'; -export default function HistogramVisType(Private) { +export default function PointSeriesVisType(Private) { const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); const Schemas = Private(VisSchemasProvider); @@ -11,35 +11,87 @@ export default function HistogramVisType(Private) { title: 'Area chart', icon: 'fa-area-chart', description: 'Great for stacked timelines in which the total of all series is more important ' + - 'than comparing any two or more series. Less useful for assessing the relative change of ' + - 'unrelated data points as changes in a series lower down the stack will have a difficult to gauge ' + - 'effect on the series above it.', + 'than comparing any two or more series. Less useful for assessing the relative change of ' + + 'unrelated data points as changes in a series lower down the stack will have a difficult to gauge ' + + 'effect on the series above it.', params: { defaults: { + grid: { + categoryLines: false, + style: { + color: '#eee' + } + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: { + }, + scale: { + type: 'linear' + }, + labels: { + show: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: { + }, + scale: { + type: 'linear', + mode: 'normal' + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100 + }, + title: {} + } + ], + seriesParams: [{ + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Count' + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1' + }], addTooltip: true, addLegend: true, legendPosition: 'right', - scale: 'linear', + showCircles: true, interpolate: 'linear', - mode: 'stacked', + scale: 'linear', + drawLinesBetweenPoints: true, + radiusRatio: 9, times: [], addTimeMarker: false, defaultYExtents: false, setYExtents: false }, - legendPositions: [{ - value: 'left', - text: 'left', - }, { - value: 'right', - text: 'right', - }, { - value: 'top', - text: 'top', - }, { - value: 'bottom', - text: 'bottom', - }], + positions: ['top', 'left', 'right', 'bottom'], + chartTypes: ['line', 'area', 'histogram'], + axisModes: ['normal', 'percentage', 'wiggle', 'silhouette'], + scaleTypes: ['linear', 'log', 'square root'], + chartModes: ['normal', 'stacked'], interpolationModes: [{ value: 'linear', text: 'straight', @@ -50,22 +102,35 @@ export default function HistogramVisType(Private) { value: 'step-after', text: 'stepped', }], - scales: ['linear', 'log', 'square root'], - modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'], - editor: areaTemplate + editor: pointSeriesTemplate, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & Axes', + editor: '
' + + '
' + }, + { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + ], }, - implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', name: 'metric', title: 'Y-Axis', min: 1, - aggFilter: '!std_dev', defaults: [ { schema: 'metric', type: 'count' } ] }, + { + group: 'metrics', + name: 'radius', + title: 'Dot Size', + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, { group: 'buckets', name: 'segment', @@ -77,7 +142,7 @@ export default function HistogramVisType(Private) { { group: 'buckets', name: 'group', - title: 'Split Area', + title: 'Split Series', min: 0, max: 1, aggFilter: '!geohash_grid' diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.html b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.html new file mode 100644 index 0000000000000..d52dff6461f05 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.html @@ -0,0 +1,113 @@ +
+
+
+ X-Axis +
+
+ + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + +
+ + + + + Show + + + Hide + + Advanced Options + + + +
+ +
+
+ Labels +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ +
diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.js new file mode 100644 index 0000000000000..c54951c9c183c --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/category_axis.js @@ -0,0 +1,32 @@ +import uiModules from 'ui/modules'; +import vislibValueAxesTemplate from 'plugins/kbn_vislib_vis_types/controls/point_series/category_axis.html'; +const module = uiModules.get('kibana'); + +module.directive('vislibCategoryAxis', function () { + return { + restrict: 'E', + template: vislibValueAxesTemplate, + replace: true, + link: function ($scope) { + $scope.rotateOptions = [ + { name: 'horizontal', value: 0 }, + { name: 'vertical', value: 90 }, + { name: 'angled', value: 75 }, + ]; + + let lastAxisTitle = ''; + $scope.$watch(() => { + return $scope.vis.aggs.map(agg => { + return agg.params.field ? agg.makeLabel() : ''; + }).join(); + }, () => { + const agg = $scope.vis.aggs.find(agg => agg.schema.name === 'segment'); + const label = agg ? agg.makeLabel() : ''; + if (lastAxisTitle !== label) { + lastAxisTitle = label; + $scope.vis.params.categoryAxes[0].title.text = label; + } + }); + } + }; +}); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html new file mode 100644 index 0000000000000..9dc1b16cd5812 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.html @@ -0,0 +1,48 @@ +
+
+
+ + + Grid + +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js new file mode 100644 index 0000000000000..3a4fdb35f4b32 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/grid.js @@ -0,0 +1,15 @@ +import uiModules from 'ui/modules'; +import vislibGridTemplate from 'plugins/kbn_vislib_vis_types/controls/point_series/grid.html'; +const module = uiModules.get('kibana'); + +module.directive('vislibGrid', function () { + return { + restrict: 'E', + template: vislibGridTemplate, + replace: true, + link: function ($scope) { + + $scope.isGridOpen = true; + } + }; +}); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js new file mode 100644 index 0000000000000..8ac21c0b03421 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/index.js @@ -0,0 +1,4 @@ +import 'plugins/kbn_vislib_vis_types/controls/point_series/value_axes.js'; +import 'plugins/kbn_vislib_vis_types/controls/point_series/category_axis.js'; +import 'plugins/kbn_vislib_vis_types/controls/point_series/series.js'; +import 'plugins/kbn_vislib_vis_types/controls/point_series/grid.js'; diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.html b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.html new file mode 100644 index 0000000000000..3b9a68327e6d1 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.html @@ -0,0 +1,118 @@ +
+
+
+ Metrics +
+
+ +
+
+
+ + + {{chart.data.label}} + +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+
+ +
+
diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.js new file mode 100644 index 0000000000000..74bb2c44d15dc --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/series.js @@ -0,0 +1,83 @@ +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import vislibSeriesTemplate from 'plugins/kbn_vislib_vis_types/controls/point_series/series.html'; +const module = uiModules.get('kibana'); + +module.directive('vislibSeries', function () { + return { + restrict: 'E', + template: vislibSeriesTemplate, + replace: true, + link: function ($scope) { + function makeSerie(label) { + const last = $scope.series[$scope.series.length - 1]; + return { + show: true, + mode: last ? last.mode : 'normal', + type: last ? last.type : 'line', + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + data: { + label: label + }, + valueAxis: $scope.vis.params.valueAxes[0].id + }; + } + + $scope.series = $scope.vis.params.seriesParams; + $scope.$watch(() => { + return $scope.vis.aggs.map(agg => { + try { + return agg.makeLabel(); + } catch (e) { + return ''; + } + }).join(); + }, () => { + const schemaTitle = $scope.vis.type.schemas.metrics[0].title; + + const metrics = $scope.vis.aggs.filter(agg => { + const isMetric = agg.type && agg.type.type === 'metrics'; + return isMetric && agg.schema.title === schemaTitle; + }); + + // update labels for existing params or create new one + $scope.vis.params.seriesParams = metrics.map((agg, i) => { + const params = $scope.vis.params.seriesParams[i]; + if (params) { + params.data.label = agg.makeLabel(); + return params; + } else { + const series = makeSerie(agg.makeLabel()); + return series; + } + }); + }); + + $scope.$watch(() => { + return $scope.vis.params.seriesParams.map(series => series.type).join(); + }, () => { + const types = _.uniq(_.map($scope.vis.params.seriesParams, 'type')); + $scope.savedVis.type = types.length === 1 ? types[0] : 'histogram'; + }); + + $scope.$watch('vis.params.valueAxes.length', () => { + $scope.vis.params.seriesParams.forEach(series => { + if (!$scope.vis.params.valueAxes.find(axis => axis.id === series.valueAxis)) { + series.valueAxis = $scope.vis.params.valueAxes[0].id; + } + }); + }); + + $scope.changeValueAxis = (index) => { + const series = $scope.vis.params.seriesParams[index]; + $scope.updateAxisTitle(); + if (series.valueAxis === 'new') { + const axis = $scope.addValueAxis(); + series.valueAxis = axis.id; + } + }; + } + }; +}); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html new file mode 100644 index 0000000000000..0b41d1ac81648 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html @@ -0,0 +1,261 @@ +
+
+
+ Y-Axes +
+ + +
+ +
+
+
+ + + {{axis.name}} + +
+ +
{{getSeriesShort(axis)}}
+ + +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + +
+ + + + + Show + + + Hide + + Advanced Options + + + +
+ + +
+
+ Labels +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + +
+
+ Custom Extents +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ Min must exceed 0 when a log scale is selected +
+
+
+
+
+
+
+ +
+
diff --git a/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js new file mode 100644 index 0000000000000..2452294118545 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js @@ -0,0 +1,161 @@ +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import vislibValueAxesTemplate from 'plugins/kbn_vislib_vis_types/controls/point_series/value_axes.html'; +const module = uiModules.get('kibana'); + +module.directive('vislibValueAxes', function () { + return { + restrict: 'E', + template: vislibValueAxesTemplate, + replace: true, + link: function ($scope) { + let isCategoryAxisHorizontal = true; + + function mapPosition(position) { + switch (position) { + case 'bottom': return 'left'; + case 'top': return 'right'; + case 'left': return 'bottom'; + case 'right': return 'top'; + } + } + + function mapPositionOposite(position) { + switch (position) { + case 'bottom': return 'top'; + case 'top': return 'bottom'; + case 'left': return 'right'; + case 'right': return 'left'; + } + } + + $scope.rotateOptions = [ + { name: 'horizontal', value: 0 }, + { name: 'vertical', value: 90 }, + { name: 'angled', value: 75 }, + ]; + + $scope.$watch('vis.params.categoryAxes[0].position', position => { + isCategoryAxisHorizontal = ['top', 'bottom'].includes(position); + $scope.vis.params.valueAxes.forEach(axis => { + const axisIsHorizontal = ['top', 'bottom'].includes(axis.position); + if (axisIsHorizontal === isCategoryAxisHorizontal) { + axis.position = mapPosition(axis.position); + $scope.updateAxisName(axis); + } + }); + }); + + $scope.getSeries = function (axis) { + const isFirst = $scope.vis.params.valueAxes[0] === axis; + const series = _.filter($scope.vis.params.seriesParams, series => { + return series.valueAxis === axis.id || (isFirst && !series.valueAxis); + }); + return series.map(series => series.data.label).join(', '); + }; + + $scope.getSeriesShort = function (axis) { + const maxStringLength = 30; + return $scope.getSeries(axis).substring(0, maxStringLength); + }; + + $scope.isPositionDisabled = function (position) { + if (isCategoryAxisHorizontal) { + return ['top', 'bottom'].includes(position); + } + return ['left', 'right'].includes(position); + }; + + $scope.addValueAxis = function () { + const firstAxis = $scope.vis.params.valueAxes[0]; + const newAxis = _.cloneDeep(firstAxis); + newAxis.id = 'ValueAxis-' + $scope.vis.params.valueAxes.reduce((value, axis) => { + if (axis.id.substr(0, 10) === 'ValueAxis-') { + const num = parseInt(axis.id.substr(10)); + if (num >= value) value = num + 1; + } + return value; + }, 1); + + newAxis.position = mapPositionOposite(firstAxis.position); + const axisName = _.capitalize(newAxis.position) + 'Axis-'; + newAxis.name = axisName + $scope.vis.params.valueAxes.reduce((value, axis) => { + if (axis.name.substr(0, axisName.length) === axisName) { + const num = parseInt(axis.name.substr(axisName.length)); + if (num >= value) value = num + 1; + } + return value; + }, 1); + + $scope.vis.params.valueAxes.push(newAxis); + return newAxis; + }; + + $scope.removeValueAxis = function (axis) { + if ($scope.vis.params.valueAxes.length > 1) { + _.remove($scope.vis.params.valueAxes, function (valAxis) { + return valAxis.id === axis.id; + }); + } + }; + + $scope.updateExtents = function (axis) { + if (!axis.scale.setYExtents) { + delete axis.scale.min; + delete axis.scale.max; + } + }; + + $scope.updateAxisName = function (axis) { + const axisName = _.capitalize(axis.position) + 'Axis-'; + axis.name = axisName + $scope.vis.params.valueAxes.reduce((value, axis) => { + if (axis.name.substr(0, axisName.length) === axisName) { + const num = parseInt(axis.name.substr(axisName.length)); + if (num >= value) value = num + 1; + } + return value; + }, 1); + }; + + const lastAxisTitles = {}; + $scope.updateAxisTitle = function () { + $scope.vis.params.valueAxes.forEach((axis, axisNumber) => { + let label = ''; + const isFirst = axisNumber === 0; + const matchingSeries = []; + $scope.vis.params.seriesParams.forEach((series, i) => { + const isMatchingSeries = (isFirst && !series.valueAxis) || (series.valueAxis === axis.id); + if (isMatchingSeries) { + let seriesNumber = 0; + $scope.vis.aggs.forEach(agg => { + if (agg.schema.name === 'metric') { + if (seriesNumber === i) matchingSeries.push(agg); + seriesNumber++; + } + }); + } + }); + if (matchingSeries.length === 1) { + label = matchingSeries[0].makeLabel(); + } + if (lastAxisTitles[axis.id] !== label) { + lastAxisTitles[axis.id] = label; + axis.title.text = label; + } + }); + }; + + $scope.$watch(() => { + return $scope.vis.aggs.map(agg => { + try { + return agg.makeLabel(); + } catch (e) { + return ''; + } + }).join(); + }, () => { + $scope.updateAxisTitle(); + }); + } + }; +}); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js b/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js new file mode 100644 index 0000000000000..9fa111489cc12 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/editors/__tests__/point_series.js @@ -0,0 +1,111 @@ +import angular from 'angular'; +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import $ from 'jquery'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import LineVisTypeProvider from 'plugins/kbn_vislib_vis_types/line'; +import VisProvider from 'ui/vis'; +import AggConfigProvider from 'ui/vis/agg_config'; + +describe('point series editor', function () { + let $parentScope; + let $scope; + let $container; + let $elem; + let lineVisType; + let Vis; + let indexPattern; + let AggConfig; + + function makeConfig() { + return { + type: 'line', + params: lineVisType.params.defaults, + aggs: [ + { type: 'count', schema: 'metric', params: { field: 'bytes' } }, + { type: 'terms', schema: 'segment', params: { field: 'machine.os' } }, + ], + listeners: { click: _.noop } + }; + } + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function ($rootScope, $compile, Private) { + AggConfig = Private(AggConfigProvider); + lineVisType = Private(LineVisTypeProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + $parentScope = $rootScope; + $parentScope.vis = new Vis(indexPattern, makeConfig()); + $parentScope.savedVis = {}; + + // share the scope + //_.defaults($parentScope, $rootScope, Object.getPrototypeOf($rootScope)); + + $container = $(document.createElement('div')) + .appendTo('body'); + // make the element + $elem = angular.element('
' + + '
'); + $container.append($elem); + + // compile the html + $compile($elem)($parentScope); + + // Digest everything + $elem.scope().$digest(); + + // give us a scope to work with + $scope = $elem.isolateScope(); + })); + + afterEach(function () { + $container.remove(); + }); + + it('should show correct series', function () { + expect($parentScope.vis.params.seriesParams.length).to.be(1); + expect($parentScope.vis.params.seriesParams[0].data.label).to.be('Count'); + }); + + it('should update series when new agg is added', function () { + const aggConfig = new AggConfig($parentScope.vis, { type: 'avg', schema: 'metric', params: { field: 'bytes' } }); + $parentScope.vis.aggs.push(aggConfig); + $parentScope.$digest(); + expect($parentScope.vis.params.seriesParams.length).to.be(2); + }); + + it('should only allow left and right value axis position when category axis is horizontal', function () { + expect($parentScope.isPositionDisabled('top')).to.be(true); + expect($parentScope.isPositionDisabled('bottom')).to.be(true); + expect($parentScope.isPositionDisabled('left')).to.be(false); + expect($parentScope.isPositionDisabled('right')).to.be(false); + }); + + it('should only allow top and bottom value axis position when category axis is vertical', function () { + $parentScope.vis.params.categoryAxes[0].position = 'left'; + $parentScope.$digest(); + expect($parentScope.vis.params.valueAxes[0].position).to.be('bottom'); + expect($parentScope.isPositionDisabled('top')).to.be(false); + expect($parentScope.isPositionDisabled('bottom')).to.be(false); + expect($parentScope.isPositionDisabled('left')).to.be(true); + expect($parentScope.isPositionDisabled('right')).to.be(true); + }); + + it('should add value axis', function () { + $parentScope.addValueAxis(); + expect($parentScope.vis.params.valueAxes.length).to.be(2); + }); + + it('should remove value axis', function () { + $parentScope.addValueAxis(); + $parentScope.removeValueAxis({ id: 'ValueAxis-2' }); + expect($parentScope.vis.params.valueAxes.length).to.be(1); + }); + + it('should not allow to remove the last value axis', function () { + $parentScope.removeValueAxis({ id: 'ValueAxis-1' }); + expect($parentScope.vis.params.valueAxes.length).to.be(1); + }); +}); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/area.html b/src/core_plugins/kbn_vislib_vis_types/public/editors/area.html deleted file mode 100644 index d19d6aa99953b..0000000000000 --- a/src/core_plugins/kbn_vislib_vis_types/public/editors/area.html +++ /dev/null @@ -1,11 +0,0 @@ - -
- -
- - - diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/histogram.html b/src/core_plugins/kbn_vislib_vis_types/public/editors/histogram.html deleted file mode 100644 index c25ae123fa84d..0000000000000 --- a/src/core_plugins/kbn_vislib_vis_types/public/editors/histogram.html +++ /dev/null @@ -1,13 +0,0 @@ - -
- - - - -
- - diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/line.html b/src/core_plugins/kbn_vislib_vis_types/public/editors/line.html deleted file mode 100644 index d15895327efc6..0000000000000 --- a/src/core_plugins/kbn_vislib_vis_types/public/editors/line.html +++ /dev/null @@ -1,23 +0,0 @@ -
- - - - -
-
- -
-
- -
-
- - diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html b/src/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html new file mode 100644 index 0000000000000..7b41658b81da9 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/editors/point_series.html @@ -0,0 +1,57 @@ +
+ +
+
+
+ Settings +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
diff --git a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js index 5790e9ce98280..5bcea96fb5da3 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -1,8 +1,8 @@ import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type'; import VisSchemasProvider from 'ui/vis/schemas'; -import histogramTemplate from 'plugins/kbn_vislib_vis_types/editors/histogram.html'; +import pointSeriesTemplate from 'plugins/kbn_vislib_vis_types/editors/point_series.html'; -export default function HistogramVisType(Private) { +export default function PointSeriesVisType(Private) { const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); const Schemas = Private(VisSchemasProvider); @@ -14,45 +14,121 @@ export default function HistogramVisType(Private) { 'exact numbers or percentages. If you are not sure which chart you need, you could do worse than to start here.', params: { defaults: { + grid: { + categoryLines: false, + style: { + color: '#eee' + } + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: { + }, + scale: { + type: 'linear' + }, + labels: { + show: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: { + }, + scale: { + type: 'linear', + mode: 'normal' + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100 + }, + title: {} + } + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count' + }, + drawLinesBetweenPoints: true, + showCircles: true + } + ], addTooltip: true, addLegend: true, legendPosition: 'right', + showCircles: true, + interpolate: 'linear', scale: 'linear', - mode: 'stacked', + drawLinesBetweenPoints: true, + radiusRatio: 9, times: [], addTimeMarker: false, defaultYExtents: false, setYExtents: false }, - legendPositions: [{ - value: 'left', - text: 'left', + positions: ['top', 'left', 'right', 'bottom'], + chartTypes: ['line', 'area', 'histogram'], + axisModes: ['normal', 'percentage', 'wiggle', 'silhouette'], + scaleTypes: ['linear', 'log', 'square root'], + chartModes: ['normal', 'stacked'], + interpolationModes: [{ + value: 'linear', + text: 'straight', }, { - value: 'right', - text: 'right', + value: 'cardinal', + text: 'smoothed', }, { - value: 'top', - text: 'top', - }, { - value: 'bottom', - text: 'bottom', + value: 'step-after', + text: 'stepped', }], - scales: ['linear', 'log', 'square root'], - modes: ['stacked', 'percentage', 'grouped'], - editor: histogramTemplate + editor: pointSeriesTemplate, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & Axes', + editor: '
' + + '
' + }, + { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + ], }, - implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', name: 'metric', title: 'Y-Axis', min: 1, - aggFilter: '!std_dev', defaults: [ { schema: 'metric', type: 'count' } ] }, + { + group: 'metrics', + name: 'radius', + title: 'Dot Size', + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, { group: 'buckets', name: 'segment', @@ -64,7 +140,7 @@ export default function HistogramVisType(Private) { { group: 'buckets', name: 'group', - title: 'Split Bars', + title: 'Split Series', min: 0, max: 1, aggFilter: '!geohash_grid' diff --git a/src/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js b/src/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js new file mode 100644 index 0000000000000..56da8ae6ab042 --- /dev/null +++ b/src/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js @@ -0,0 +1,157 @@ +import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type'; +import VisSchemasProvider from 'ui/vis/schemas'; +import pointSeriesTemplate from 'plugins/kbn_vislib_vis_types/editors/point_series.html'; + +export default function PointSeriesVisType(Private) { + const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); + const Schemas = Private(VisSchemasProvider); + + return new VislibVisType({ + name: 'horizontal_bar', + title: 'Horizontal bar chart', + icon: 'fa-bars', + description: 'Like histogram chart but with horizontal bars.', + params: { + defaults: { + grid: { + categoryLines: false, + style: { + color: '#eee' + } + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'left', + show: true, + style: { + }, + scale: { + type: 'linear' + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 200 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'bottom', + show: true, + style: { + }, + scale: { + type: 'linear', + mode: 'normal' + }, + labels: { + show: true, + rotate: 75, + filter: true, + truncate: 100 + }, + title: {} + } + ], + seriesParams: [{ + show: true, + type: 'histogram', + mode: 'normal', + data: { + label: 'Count' + }, + drawLinesBetweenPoints: true, + showCircles: true + }], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + showCircles: true, + interpolate: 'linear', + scale: 'linear', + drawLinesBetweenPoints: true, + radiusRatio: 9, + times: [], + addTimeMarker: false, + defaultYExtents: false, + setYExtents: false + }, + positions: ['top', 'left', 'right', 'bottom'], + chartTypes: ['line', 'area', 'histogram'], + axisModes: ['normal', 'percentage', 'wiggle', 'silhouette'], + scaleTypes: ['linear', 'log', 'square root'], + chartModes: ['normal', 'stacked'], + interpolationModes: [{ + value: 'linear', + text: 'straight', + }, { + value: 'cardinal', + text: 'smoothed', + }, { + value: 'step-after', + text: 'stepped', + }], + editor: pointSeriesTemplate, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & Axes', + editor: '
' + + '
' + }, + { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + ], + }, + schemas: new Schemas([ + { + group: 'metrics', + name: 'metric', + title: 'Y-Axis', + min: 1, + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: 'metrics', + name: 'radius', + title: 'Dot Size', + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, + { + group: 'buckets', + name: 'segment', + title: 'X-Axis', + min: 0, + max: 1, + aggFilter: '!geohash_grid' + }, + { + group: 'buckets', + name: 'group', + title: 'Split Series', + min: 0, + max: 1, + aggFilter: '!geohash_grid' + }, + { + group: 'buckets', + name: 'split', + title: 'Split Chart', + min: 0, + max: 1, + aggFilter: '!geohash_grid' + } + ]) + }); +} diff --git a/src/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js b/src/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js index 1761907b85d47..05b23e39f17ac 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js @@ -6,6 +6,7 @@ import pieVisTypeProvider from 'plugins/kbn_vislib_vis_types/pie'; import areaVisTypeProvider from 'plugins/kbn_vislib_vis_types/area'; import tileMapVisTypeProvider from 'plugins/kbn_vislib_vis_types/tile_map'; import heatmapVisTypeProvider from 'plugins/kbn_vislib_vis_types/heatmap'; +import horizontalBarVisTypeProvider from 'plugins/kbn_vislib_vis_types/horizontal_bar'; visTypes.register(histogramVisTypeProvider); visTypes.register(lineVisTypeProvider); @@ -13,3 +14,4 @@ visTypes.register(pieVisTypeProvider); visTypes.register(areaVisTypeProvider); visTypes.register(tileMapVisTypeProvider); visTypes.register(heatmapVisTypeProvider); +visTypes.register(horizontalBarVisTypeProvider); diff --git a/src/core_plugins/kbn_vislib_vis_types/public/line.js b/src/core_plugins/kbn_vislib_vis_types/public/line.js index 2f74f681a5d3d..861b2e0e5cf89 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/line.js @@ -1,8 +1,8 @@ import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type'; import VisSchemasProvider from 'ui/vis/schemas'; -import lineTemplate from 'plugins/kbn_vislib_vis_types/editors/line.html'; +import pointSeriesTemplate from 'plugins/kbn_vislib_vis_types/editors/point_series.html'; -export default function HistogramVisType(Private) { +export default function PointSeriesVisType(Private) { const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); const Schemas = Private(VisSchemasProvider); @@ -11,9 +11,56 @@ export default function HistogramVisType(Private) { title: 'Line chart', icon: 'fa-line-chart', description: 'Often the best chart for high density time series. Great for comparing one series to another. ' + - 'Be careful with sparse sets as the connection between points can be misleading.', + 'Be careful with sparse sets as the connection between points can be misleading.', params: { defaults: { + grid: { + categoryLines: false, + style: { + color: '#eee' + } + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: { + }, + scale: { + type: 'linear' + }, + labels: { + show: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: { + }, + scale: { + type: 'linear', + mode: 'normal' + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100 + }, + title: {} + } + ], + seriesParams: [], addTooltip: true, addLegend: true, legendPosition: 'right', @@ -27,19 +74,11 @@ export default function HistogramVisType(Private) { defaultYExtents: false, setYExtents: false }, - legendPositions: [{ - value: 'left', - text: 'left', - }, { - value: 'right', - text: 'right', - }, { - value: 'top', - text: 'top', - }, { - value: 'bottom', - text: 'bottom', - }], + positions: ['top', 'left', 'right', 'bottom'], + chartTypes: ['line', 'area', 'histogram'], + axisModes: ['normal', 'percentage', 'wiggle', 'silhouette'], + scaleTypes: ['linear', 'log', 'square root'], + chartModes: ['normal', 'stacked'], interpolationModes: [{ value: 'linear', text: 'straight', @@ -50,10 +89,17 @@ export default function HistogramVisType(Private) { value: 'step-after', text: 'stepped', }], - scales: ['linear', 'log', 'square root'], - editor: lineTemplate + editor: pointSeriesTemplate, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & Axes', + editor: '
' + + '
' + }, + { name: 'options', title: 'Panel Settings', editor: pointSeriesTemplate }, + ], }, - implementsRenderComplete: true, schemas: new Schemas([ { group: 'metrics', @@ -83,7 +129,7 @@ export default function HistogramVisType(Private) { { group: 'buckets', name: 'group', - title: 'Split Lines', + title: 'Split Series', min: 0, max: 1, aggFilter: '!geohash_grid' diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.html b/src/core_plugins/kibana/public/visualize/editor/editor.html index 93021aaf27f49..db4e89fcffec0 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.html +++ b/src/core_plugins/kibana/public/visualize/editor/editor.html @@ -75,7 +75,7 @@ class="vis-editor-full-options">
- +
diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.js b/src/core_plugins/kibana/public/visualize/editor/editor.js index 0d4904fea9556..54fdbbcda634d 100644 --- a/src/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/core_plugins/kibana/public/visualize/editor/editor.js @@ -293,6 +293,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie $scope.doSave = function () { // vis.title was not bound and it's needed to reflect title into visState $state.vis.title = savedVis.title; + $state.vis.type = savedVis.type || $state.vis.type; savedVis.visState = $state.vis; savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); diff --git a/src/core_plugins/kibana/public/visualize/editor/sidebar.html b/src/core_plugins/kibana/public/visualize/editor/sidebar.html index b7c5627e56f92..2037f40234e05 100644 --- a/src/core_plugins/kibana/public/visualize/editor/sidebar.html +++ b/src/core_plugins/kibana/public/visualize/editor/sidebar.html @@ -1,7 +1,7 @@ -
- - +
+
-
diff --git a/src/core_plugins/kibana/public/visualize/editor/vis_options.js b/src/core_plugins/kibana/public/visualize/editor/vis_options.js index 1b8163f159b28..ae94d781a2d68 100644 --- a/src/core_plugins/kibana/public/visualize/editor/vis_options.js +++ b/src/core_plugins/kibana/public/visualize/editor/vis_options.js @@ -13,10 +13,11 @@ uiModules vis: '=', savedVis: '=', uiState: '=', + editor: '=' }, link: function ($scope, $el) { const $optionContainer = $el.find('.visualization-options'); - const $editor = $compile($scope.vis.type.params.editor)($scope); + const $editor = $compile($scope.editor)($scope); $optionContainer.append($editor); $scope.$watch('vis.type.schemas.all.length', function (len) { diff --git a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js index c07780bfbf0a2..5b23af025bd0e 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js @@ -13,7 +13,7 @@ describe('addToSiri', function () { const series = new Map(); const point = {}; const id = 'id'; - addToSiri(series, point, id); + addToSiri(series, point, id, id, { id: id }); expect(series.has(id)).to.be(true); expect(series.get(id)).to.be.an('object'); @@ -27,10 +27,10 @@ describe('addToSiri', function () { const id = 'id'; const point = {}; - addToSiri(series, point, id); + addToSiri(series, point, id, id, { id: id }); const point2 = {}; - addToSiri(series, point2, id); + addToSiri(series, point2, id, id, { id: id }); expect(series.has(id)).to.be(true); expect(series.get(id)).to.be.an('object'); @@ -45,7 +45,7 @@ describe('addToSiri', function () { const id = 'id'; const label = 'label'; const point = {}; - addToSiri(series, point, id, label); + addToSiri(series, point, id, label, { id: id }); expect(series.has(id)).to.be(true); expect(series.get(id)).to.be.an('object'); diff --git a/src/ui/public/agg_response/point_series/__tests__/_get_series.js b/src/ui/public/agg_response/point_series/__tests__/_get_series.js index 5c1ac921ce870..d906e4db6e541 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_get_series.js +++ b/src/ui/public/agg_response/point_series/__tests__/_get_series.js @@ -31,7 +31,7 @@ describe('getSeries', function () { const chart = { aspects: { x: { i: 0 }, - y: { i: 1, col: yCol }, + y: { i: 1, col: yCol, agg: { id: 'id' } }, z: { i: 2 } } }; @@ -119,7 +119,7 @@ describe('getSeries', function () { aspects: { x: { i: -1 }, series: { i: 0, agg: agg }, - y: { i: 1, col: { title: '0' } } + y: { i: 1, col: { title: '0' }, agg: agg } } }; diff --git a/src/ui/public/agg_response/point_series/_add_to_siri.js b/src/ui/public/agg_response/point_series/_add_to_siri.js index 1ed56b53c3557..079dc4c6b962a 100644 --- a/src/ui/public/agg_response/point_series/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/_add_to_siri.js @@ -1,5 +1,5 @@ export default function PointSeriesAddToSiri() { - return function addToSiri(series, point, id, label) { + return function addToSiri(series, point, id, label, agg) { id = id == null ? '' : id + ''; if (series.has(id)) { @@ -9,6 +9,9 @@ export default function PointSeriesAddToSiri() { series.set(id, { label: label == null ? id : label, + aggLabel: agg.type ? agg.type.makeLabel(agg) : label, + aggId: agg.parentId ? agg.parentId : agg.id, + count: 0, values: [point] }); }; diff --git a/src/ui/public/agg_response/point_series/_get_series.js b/src/ui/public/agg_response/point_series/_get_series.js index 20a1fc9fa1e27..1119f324e5069 100644 --- a/src/ui/public/agg_response/point_series/_get_series.js +++ b/src/ui/public/agg_response/point_series/_get_series.js @@ -15,7 +15,7 @@ export default function PointSeriesGetSeries(Private) { .transform(function (series, row) { if (!multiY) { const point = partGetPoint(row, aspects.y, aspects.z); - if (point) addToSiri(series, point, point.series); + if (point) addToSiri(series, point, point.series, point.series, aspects.y.agg); return; } @@ -35,7 +35,7 @@ export default function PointSeriesGetSeries(Private) { seriesLabel = prefix + seriesLabel; } - addToSiri(series, point, seriesId, seriesLabel); + addToSiri(series, point, seriesId, seriesLabel, y.agg); }); }, new Map()) diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 4da92ce29b6d4..843aa58fba443 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -903,6 +903,7 @@ fieldset { .kuiSideBarSelect { // TODO: @include kuiSelect styles when this is moved to the UI Framework and we're using SASS. height: 24px; + width: 100%; font-size: 12px; padding: 0 15px; } @@ -923,6 +924,10 @@ fieldset { margin-bottom: 6px; } +.kuiSideBarSection__main { + margin-bottom: 25px; +} + .kuiSideBarSectionTitle { display: flex; justify-content: space-between; @@ -931,6 +936,11 @@ fieldset { border-bottom: 1px solid #D4D4D4; } +.kuiSideBarSection__main .kuiSideBarSectionTitle { + background-color: #E4E4E4; + padding: 2px 10px; +} + .kuiSideBarSectionTitle__text { font-size: 14px; font-weight: 700; diff --git a/src/ui/public/vis/__tests__/_vis.js b/src/ui/public/vis/__tests__/_vis.js index 0dd724a06458c..8b6fb55001312 100644 --- a/src/ui/public/vis/__tests__/_vis.js +++ b/src/ui/public/vis/__tests__/_vis.js @@ -92,7 +92,6 @@ describe('Vis Class', function () { expect(vis).to.have.property('params'); expect(vis.params).to.have.property('addLegend', true); expect(vis.params).to.have.property('addTooltip', true); - expect(vis.params).to.have.property('mode', 'stacked'); }); }); diff --git a/src/ui/public/vis/vis_type.js b/src/ui/public/vis/vis_type.js index 011e92b4ed06d..626e57eabd771 100644 --- a/src/ui/public/vis/vis_type.js +++ b/src/ui/public/vis/vis_type.js @@ -17,6 +17,12 @@ export default function VisTypeFactory(Private) { this.requiresSearch = opts.requiresSearch == null ? true : opts.requiresSearch; // Default to true unless otherwise specified this.fullEditor = opts.fullEditor == null ? false : opts.fullEditor; this.implementsRenderComplete = opts.implementsRenderComplete || false; + + if (!this.params.optionTabs) { + this.params.optionTabs = [ + { name: 'options', title: 'Options', editor: this.params.editor } + ]; + } } VisType.prototype.createRenderbot = function (vis, $el, uiState) { diff --git a/src/ui/public/vislib/__tests__/lib/axis/axis.js b/src/ui/public/vislib/__tests__/lib/axis/axis.js new file mode 100644 index 0000000000000..5795163058400 --- /dev/null +++ b/src/ui/public/vislib/__tests__/lib/axis/axis.js @@ -0,0 +1,228 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import $ from 'jquery'; +import VislibLibDataProvider from 'ui/vislib/lib/data'; +import 'ui/persisted_state'; +import VislibLibAxisProvider from 'ui/vislib/lib/axis'; +import VislibVisConfig from 'ui/vislib/lib/vis_config'; + +describe('Vislib Axis Class Test Suite', function () { + let Axis; + let Data; + let persistedState; + let yAxis; + let el; + let fixture; + let VisConfig; + let seriesData; + + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458 + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8 + }, + { + x: 1408734090000, + y: 23 + }, + { + x: 1408734120000, + y: 30 + }, + { + x: 1408734130000, + y: 30 + }, + { + x: 1408734150000, + y: 28 + } + ] + }, + { + label: 'Count2', + values: [ + { + x: 1408734060000, + y: 8 + }, + { + x: 1408734090000, + y: 23 + }, + { + x: 1408734120000, + y: 30 + }, + { + x: 1408734140000, + y: 30 + }, + { + x: 1408734150000, + y: 28 + } + ] + } + ], + xAxisFormatter: function (thing) { + return new Date(thing); + }, + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count' + }; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private, $injector) { + Data = Private(VislibLibDataProvider); + persistedState = new ($injector.get('PersistedState'))(); + Axis = Private(VislibLibAxisProvider); + VisConfig = Private(VislibVisConfig); + + el = d3.select('body').append('div') + .attr('class', 'x-axis-wrapper') + .style('height', '40px'); + + fixture = el.append('div') + .attr('class', 'x-axis-div'); + + const visConfig = new VisConfig({ + type: 'histogram' + }, data, persistedState, $('.x-axis-div')[0]); + yAxis = new Axis(visConfig, { + type: 'value', + id: 'ValueAxis-1' + }); + + seriesData = data.series.map(series => { + return series.values; + }); + })); + + afterEach(function () { + fixture.remove(); + el.remove(); + }); + + describe('_stackNegAndPosVals Method', function () { + + it('should correctly stack positive values', function () { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8 + }, + { + x: 1408734090000, + y: 23, + y0: 23 + }, + { + x: 1408734120000, + y: 30, + y0: 30 + }, + { + x: 1408734140000, + y: 30, + y0: 0 + }, + { + x: 1408734150000, + y: 28, + y0: 28 + } + ]; + const stackedData = yAxis._stackNegAndPosVals(seriesData); + expect(stackedData[1]).to.eql(expectedResult); + }); + + it('should correctly stack pos and neg values', function () { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 0 + }, + { + x: 1408734090000, + y: 23, + y0: 0 + }, + { + x: 1408734120000, + y: 30, + y0: 0 + }, + { + x: 1408734140000, + y: 30, + y0: 0 + }, + { + x: 1408734150000, + y: 28, + y0: 0 + } + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach(value => { + value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).to.eql(expectedResult); + }); + + it('should correctly stack mixed pos and neg values', function () { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8 + }, + { + x: 1408734090000, + y: 23, + y0: 0 + }, + { + x: 1408734120000, + y: 30, + y0: 30 + }, + { + x: 1408734140000, + y: 30, + y0: 0 + }, + { + x: 1408734150000, + y: 28, + y0: 28 + } + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach((value, i) => { + if ((i % 2) === 1) value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).to.eql(expectedResult); + }); + + }); + +}); diff --git a/src/ui/public/vislib/lib/axis/axis.js b/src/ui/public/vislib/lib/axis/axis.js index a08bf1a99ff5b..8edd61a3c75fe 100644 --- a/src/ui/public/vislib/lib/axis/axis.js +++ b/src/ui/public/vislib/lib/axis/axis.js @@ -43,124 +43,21 @@ export default function AxisFactory(Private) { const stackedMode = ['normal', 'grouped'].includes(this.axisConfig.get('scale.mode')); if (stackedMode) { - this.stack.out((d, y0, y) => { - return this._stackNegAndPosVals(d, y0, y); - }); + const self = this; + this.stack = this._stackNegAndPosVals; } } - /** - * Returns true for positive numbers - */ - _isPositive(num) { - return num >= 0; - } - - /** - * Returns true for negative numbers - */ - _isNegative(num) { - return num < 0; - } - - /** - * Adds two input values - */ - _addVals(a, b) { - return a + b; - } - - /** - * Returns the results of the addition of numbers in a filtered array. - */ - _sumYs(arr, callback) { - const filteredArray = arr.filter(callback); - - return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; - } - - /** - * Calculates the d.y0 value for stacked data in D3. - */ - _calcYZero(y, arr) { - if (y === 0 && this._lastY0) return this._sumYs(arr, this._lastY0 > 0 ? this._isPositive : this._isNegative); - if (y >= 0) return this._sumYs(arr, this._isPositive); - return this._sumYs(arr, this._isNegative); - } - - _getCounts(i, j) { - const data = this.visConfig.data.chartData(); - const dataLengths = {}; - - dataLengths.charts = data.length; - dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; - dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; - - return dataLengths; - } - - _createCache() { - const cache = { - index: { - chart: 0, - stack: 0, - value: 0 - }, - yValsArr: [] - }; - - cache.count = this._getCounts(cache.index.chart, cache.index.stack); - - return cache; - } - /** - * Stacking function passed to the D3 Stack Layout `.out` API. - * See: https://github.com/mbostock/d3/wiki/Stack-Layout - * It is responsible for calculating the correct d.y0 value for - * mixed datasets containing both positive and negative values. - */ - _stackNegAndPosVals(d, y0, y) { - const data = this.visConfig.data.chartData(); - - // Storing counters and data characteristics needed to stack values properly - if (!this._cache) { - this._cache = this._createCache(); - } - - d.y0 = this._calcYZero(y, this._cache.yValsArr); - if (d.y0 > 0) this._lastY0 = 1; - if (d.y0 < 0) this._lastY0 = -1; - ++this._cache.index.stack; - - - // last stack, or last value, reset the stack count and y value array - const lastStack = (this._cache.index.stack >= this._cache.count.stacks); - if (lastStack) { - this._cache.index.stack = 0; - ++this._cache.index.value; - this._cache.yValsArr = []; - // still building the stack collection, push v value to array - } else if (y !== 0) { - this._cache.yValsArr.push(y); - } - - // last value, prepare for the next chart, if one exists - const lastValue = (this._cache.index.value >= this._cache.count.values); - if (lastValue) { - this._cache.index.value = 0; - ++this._cache.index.chart; - - // no more charts, reset the queue and finish - if (this._cache.index.chart >= this._cache.count.charts) { - this._cache = this._createCache(); - return; - } - - // get stack and value count for next chart - const chartSeries = data[this._cache.index.chart].series; - this._cache.count.stacks = chartSeries.length; - this._cache.count.values = chartSeries.length ? chartSeries[this._cache.index.stack].values.length : 0; - } + _stackNegAndPosVals(data) { + const cache = {}; + data.forEach(series => { + series.forEach(value => { + if (!cache[value.x]) cache[value.x] = [0, 0]; + value.y0 = cache[value.x][value.y < 0 ? 0 : 1]; + cache[value.x][value.y < 0 ? 0 : 1] += value.y; + }); + }); + return data; } render() { @@ -173,6 +70,7 @@ export default function AxisFactory(Private) { const elSelector = this.axisConfig.get('elSelector'); const rootEl = this.axisConfig.get('rootEl'); $(rootEl).find(elSelector).find('svg').remove(); + this.axisTitle.destroy(); } getAxis(length) { @@ -271,13 +169,14 @@ export default function AxisFactory(Private) { } draw() { + let svg; const self = this; const config = this.axisConfig; const style = config.get('style'); return function (selection) { const n = selection[0].length; - if (config.get('show') && self.axisTitle) { + if (config.get('show') && self.axisTitle && ['left', 'top'].includes(config.get('position'))) { self.axisTitle.render(selection); } selection.each(function () { @@ -292,7 +191,7 @@ export default function AxisFactory(Private) { const axis = self.getAxis(length); if (config.get('show')) { - const svg = div.append('svg') + svg = div.append('svg') .attr('width', width) .attr('height', height); @@ -313,9 +212,14 @@ export default function AxisFactory(Private) { .style('stroke-opacity', style.opacity); } if (self.axisLabels) self.axisLabels.render(svg); - svg.call(self.adjustSize()); } }); + + if (self.axisTitle && ['right', 'bottom'].includes(config.get('position'))) { + self.axisTitle.render(selection); + } + + if (svg) svg.call(self.adjustSize()); }; } } diff --git a/src/ui/public/vislib/lib/axis/axis_config.js b/src/ui/public/vislib/lib/axis/axis_config.js index c85f13db2fbb0..ea31c89797d72 100644 --- a/src/ui/public/vislib/lib/axis/axis_config.js +++ b/src/ui/public/vislib/lib/axis/axis_config.js @@ -42,7 +42,7 @@ export default function AxisConfigFactory() { }, title: { text: '', - elSelector: '.axis-wrapper-{pos} .axis-title' + elSelector: '.axis-wrapper-{pos} .axis-div', } }; @@ -77,8 +77,11 @@ export default function AxisConfigFactory() { const typeDefaults = axisConfigArgs.type === 'category' ? categoryDefaults : valueDefaults; // _.defaultsDeep mutates axisConfigArgs nested values so we clone it first const axisConfigArgsClone = _.cloneDeep(axisConfigArgs); + const isCategoryAxis = axisConfigArgsClone.type === 'category'; + const isHorizontal = axisConfigArgsClone.position && ['top', 'bottom'].includes(axisConfigArgsClone.position); + + _.merge(typeDefaults, isHorizontal || isCategoryAxis ? horizontalDefaults : verticalDefaults); 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'); diff --git a/src/ui/public/vislib/lib/axis/axis_labels.js b/src/ui/public/vislib/lib/axis/axis_labels.js index a525f35514117..a39f2a721d457 100644 --- a/src/ui/public/vislib/lib/axis/axis_labels.js +++ b/src/ui/public/vislib/lib/axis/axis_labels.js @@ -1,6 +1,5 @@ import d3 from 'd3'; import $ from 'jquery'; -import _ from 'lodash'; export default function AxisLabelsFactory(Private) { class AxisLabels { constructor(axisConfig, scale) { @@ -19,29 +18,37 @@ export default function AxisLabelsFactory(Private) { if (config.get('labels.rotate')) { text - .style('text-anchor', function () { - return config.get('labels.rotateAnchor') === 'center' ? 'center' : 'end'; - }) - .attr('dy', function () { - if (config.isHorizontal()) { - if (config.get('position') === 'top') return '-0.9em'; - else return '0.3em'; + .style('text-anchor', function (d, i) { + const currentValue = $(this).css('text-anchor'); + const rotateDeg = config.get('labels.rotate'); + if (!rotateDeg) return currentValue; + else { + const position = config.get('position'); + switch (position) { + case 'top': return 'end'; + case 'bottom': return 'end'; + default: + if (rotateDeg === 90 || rotateDeg === -90) return 'middle'; + return currentValue; + } } - return '0'; }) - .attr('dx', function () { - return config.isHorizontal() ? '-0.9em' : '0'; + .attr('dy', function () { + return config.isHorizontal() ? '0.3em' : '0'; }) .attr('transform', function rotate(d, j) { - let rotateDeg = config.get('labels.rotate'); - if (config.get('labels.rotateAnchor') === 'center') { + const position = config.get('position'); + const rotateDeg = position === 'top' ? config.get('labels.rotate') : -config.get('labels.rotate'); + + if ($(this).css('text-anchor') === 'middle') { const coord = text[0][j].getBBox(); const transX = ((coord.x) + (coord.width / 2)); const transY = ((coord.y) + (coord.height / 2)); return `rotate(${rotateDeg}, ${transX}, ${transY})`; } else { - rotateDeg = config.get('position') === 'top' ? rotateDeg : -rotateDeg; - return `rotate(${rotateDeg})`; + const transX = this.attributes.x.nodeValue; + const transY = this.attributes.y.nodeValue; + return `rotate(${rotateDeg}, ${transX}, ${transY})`; } }); } diff --git a/src/ui/public/vislib/lib/axis/axis_title.js b/src/ui/public/vislib/lib/axis/axis_title.js index c0e15bf0fb78c..6b299f8dc87f6 100644 --- a/src/ui/public/vislib/lib/axis/axis_title.js +++ b/src/ui/public/vislib/lib/axis/axis_title.js @@ -12,6 +12,10 @@ export default function AxisTitleFactory(Private) { d3.select(this.axisConfig.get('rootEl')).selectAll(this.elSelector).call(this.draw()); } + destroy() { + $(this.axisConfig.get('rootEl')).find(this.elSelector).find('svg').remove(); + } + draw() { const config = this.axisConfig; @@ -23,6 +27,7 @@ export default function AxisTitleFactory(Private) { const div = d3.select(el); const width = $(el).width(); const height = $(el).height(); + const titlePadding = 15; const svg = div.append('svg') .attr('width', width) @@ -31,9 +36,9 @@ export default function AxisTitleFactory(Private) { const bbox = svg.append('text') .attr('transform', function () { if (config.isHorizontal()) { - return 'translate(' + width / 2 + ',11)'; + return `translate(${width / 2},${titlePadding})`; } - return 'translate(11,' + height / 2 + ') rotate(270)'; + return `translate(${titlePadding},${height / 2}) rotate(270)`; }) .attr('text-anchor', 'middle') .text(config.get('title.text')) diff --git a/src/ui/public/vislib/lib/chart_grid.js b/src/ui/public/vislib/lib/chart_grid.js new file mode 100644 index 0000000000000..ede01269b8639 --- /dev/null +++ b/src/ui/public/vislib/lib/chart_grid.js @@ -0,0 +1,79 @@ +import d3 from 'd3'; +import _ from 'lodash'; +export default function ChartTitleFactory(Private) { + + const defaults = { + style: { + color: '#eee' + }, + categoryLines: false, + valueAxis: undefined, + }; + + class ChartGrid { + constructor(handler, gridConfig) { + if (!gridConfig) return; + this._handler = handler; + this._values = _.defaultsDeep({}, gridConfig, defaults); + } + + drawLine(svg, tick, axis, width, height) { + const isHorizontal = axis.axisConfig.isHorizontal(); + const scale = axis.getScale(); + svg.append('path') + .attr('d', () => { + const x0 = isHorizontal ? tick : 0; + const x1 = isHorizontal ? tick : width; + const y0 = !isHorizontal ? tick : 0; + const y1 = !isHorizontal ? tick : height; + const d3Line = d3.svg.line() + .x(d => isHorizontal ? scale(d[0]) : d[0]) + .y(d => !isHorizontal ? scale(d[1]) : d[1]); + return d3Line([[x0, y0], [x1, y1]]); + }) + .attr('fill', 'none') + .attr('stroke', this.get('style.color')) + .attr('stroke-width', 1); + } + + drawCategoryLines(svg, width, height) { + const axis = this._handler.categoryAxes[0]; + axis.getScale().ticks().forEach(tick => { + this.drawLine(svg, tick, axis, width, height); + }); + } + + drawValueLines(svg, width, height) { + const axis = this._handler.valueAxes.find(axis => axis.axisConfig.get('id') === this.get('valueAxis')); + axis.getScale().ticks().forEach(tick => { + this.drawLine(svg, tick, axis, width, height); + }); + } + + draw(width, height) { + const self = this; + return function (selection) { + if (!self._values) return; + selection.each(function () { + if (self.get('categoryLines')) self.drawCategoryLines(d3.select(this), width, height); + if (self.get('valueAxis', false)) self.drawValueLines(d3.select(this), width, height); + }); + }; + } + + get(property, defaults) { + if (_.has(this._values, property) || typeof defaults !== 'undefined') { + return _.get(this._values, property, defaults); + } else { + throw new Error(`Accessing invalid config property: ${property}`); + return defaults; + } + } + + set(property, value) { + return _.set(this._values, property, value); + } + } + + return ChartGrid; +} diff --git a/src/ui/public/vislib/lib/data.js b/src/ui/public/vislib/lib/data.js index 639fa5e9457bc..2a093cee533cb 100644 --- a/src/ui/public/vislib/lib/data.js +++ b/src/ui/public/vislib/lib/data.js @@ -41,6 +41,8 @@ export default function DataFactory(Private) { newData[key] = data[key].map(seri => { return { label: seri.label, + aggLabel: seri.aggLabel, + aggId: seri.aggId, values: seri.values.map(val => { const newVal = _.clone(val); newVal.aggConfig = val.aggConfig; @@ -111,11 +113,7 @@ export default function DataFactory(Private) { 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; + return (seriesConfig.mode === 'stacked'); } getStackedSeries(chartConfig, axis, series, first = false) { @@ -135,6 +133,7 @@ export default function DataFactory(Private) { const id = axis.axisConfig.get('id'); stackedData[id] = this.getStackedSeries(chartConfig, axis, data, i === 0); stackedData[id] = this.injectZeros(stackedData[id], handler.visConfig.get('orderBucketsBySum', false)); + axis.axisConfig.set('stackedSeries', stackedData[id].length); axis.stack(_.map(stackedData[id], 'values')); }); return stackedData; diff --git a/src/ui/public/vislib/lib/handler.js b/src/ui/public/vislib/lib/handler.js index 2e20650965ea0..6045c9b6f51eb 100644 --- a/src/ui/public/vislib/lib/handler.js +++ b/src/ui/public/vislib/lib/handler.js @@ -7,6 +7,7 @@ import VislibLibLayoutLayoutProvider from './layout/layout'; import VislibLibChartTitleProvider from './chart_title'; import VislibLibAlertsProvider from './alerts'; import VislibAxisProvider from './axis/axis'; +import VislibGridProvider from './chart_grid'; import VislibVisualizationsVisTypesProvider from '../visualizations/vis_types'; export default function HandlerBaseClass(Private) { @@ -15,6 +16,7 @@ export default function HandlerBaseClass(Private) { const ChartTitle = Private(VislibLibChartTitleProvider); const Alerts = Private(VislibLibAlertsProvider); const Axis = Private(VislibAxisProvider); + const Grid = Private(VislibGridProvider); /** * Handles building all the components of the visualization @@ -39,6 +41,7 @@ export default function HandlerBaseClass(Private) { this.valueAxes = visConfig.get('valueAxes').map(axisArgs => new Axis(visConfig, axisArgs)); this.chartTitle = new ChartTitle(visConfig); this.alerts = new Alerts(this, visConfig.get('alerts')); + this.grid = new Grid(this, visConfig.get('grid')); if (visConfig.get('type') === 'point_series') { this.data.stackData(this); diff --git a/src/ui/public/vislib/lib/types/index.js b/src/ui/public/vislib/lib/types/index.js index 350aa123bf60e..23a090293040b 100644 --- a/src/ui/public/vislib/lib/types/index.js +++ b/src/ui/public/vislib/lib/types/index.js @@ -11,6 +11,7 @@ export default function TypeFactory(Private) { */ return { histogram: pointSeries.column, + horizontal_bar: pointSeries.column, line: pointSeries.line, pie: Private(VislibLibTypesPieProvider), area: pointSeries.area, diff --git a/src/ui/public/vislib/lib/types/point_series.js b/src/ui/public/vislib/lib/types/point_series.js index 558b9dd68f977..c08ff9eb992d2 100644 --- a/src/ui/public/vislib/lib/types/point_series.js +++ b/src/ui/public/vislib/lib/types/point_series.js @@ -1,27 +1,49 @@ import _ from 'lodash'; -import errors from 'ui/errors'; export default function ColumnHandler(Private) { - const createSeries = (cfg, series) => { - const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(cfg.mode); - let interpolate = cfg.interpolate; + const createSerieFromParams = (cfg, seri) => { + const matchingSeriParams = cfg.seriesParams ? cfg.seriesParams.find(seriConfig => { + return seri.aggLabel === seriConfig.data.label; + }) : null; + + + let interpolate = matchingSeriParams ? matchingSeriParams.interpolate : cfg.interpolate; // for backward compatibility when loading URLs or configs we need to check smoothLines if (cfg.smoothLines) interpolate = 'cardinal'; + if (!matchingSeriParams) { + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(cfg.mode); + return { + show: true, + type: cfg.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: interpolate, + drawLinesBetweenPoints: cfg.drawLinesBetweenPoints, + showCircles: cfg.showCircles, + radiusRatio: cfg.radiusRatio, + data: seri + }; + } + + return { + show: matchingSeriParams.show, + type: matchingSeriParams.type, + mode: matchingSeriParams.mode, + interpolate: interpolate, + valueAxis: matchingSeriParams.valueAxis, + drawLinesBetweenPoints: matchingSeriParams.drawLinesBetweenPoints, + showCircles: matchingSeriParams.showCircles, + radiusRatio: matchingSeriParams.radiusRatio, + data: seri + }; + }; + + const createSeries = (cfg, series) => { return { type: 'point_series', series: _.map(series, (seri) => { - return { - show: true, - type: cfg.type || 'line', - mode: stacked ? 'stacked' : 'normal', - interpolate: interpolate, - drawLinesBetweenPoints: cfg.drawLinesBetweenPoints, - showCircles: cfg.showCircles, - radiusRatio: cfg.radiusRatio, - data: seri - }; + return createSerieFromParams(cfg, seri); }) }; }; diff --git a/src/ui/public/vislib/lib/vis_config.js b/src/ui/public/vislib/lib/vis_config.js index 7d1d70507bd11..16d1bda6d90ba 100644 --- a/src/ui/public/vislib/lib/vis_config.js +++ b/src/ui/public/vislib/lib/vis_config.js @@ -15,7 +15,8 @@ export default function VisConfigFactory(Private) { }, alerts: [], categoryAxes: [], - valueAxes: [] + valueAxes: [], + grid: {} }; diff --git a/src/ui/public/vislib/styles/_layout.less b/src/ui/public/vislib/styles/_layout.less index 3b6076c9333b9..8429a5f353427 100644 --- a/src/ui/public/vislib/styles/_layout.less +++ b/src/ui/public/vislib/styles/_layout.less @@ -187,6 +187,10 @@ width: 100%; } +.x-axis-div svg { + float: left; /* for some reason svg wont get positioned in top left corner of container div without this */ +} + .axis-wrapper-top .axis-div svg { margin-bottom: -5px; } diff --git a/src/ui/public/vislib/styles/_svg.less b/src/ui/public/vislib/styles/_svg.less index ae2fd14a73792..a1fd676b93f0f 100644 --- a/src/ui/public/vislib/styles/_svg.less +++ b/src/ui/public/vislib/styles/_svg.less @@ -28,7 +28,6 @@ /* SVG Element Default Styling */ rect { - stroke: none; opacity: 1; &:hover { @@ -50,6 +49,12 @@ circle { opacity: 0.6; } +.series > path, +.series > rect { + fill-opacity: 0.6; + stroke-opacity: 1; +} + .blur_shape { opacity: 0.3 !important; } diff --git a/src/ui/public/vislib/visualizations/point_series.js b/src/ui/public/vislib/visualizations/point_series.js index 1847df56875cd..f9155f140ced9 100644 --- a/src/ui/public/vislib/visualizations/point_series.js +++ b/src/ui/public/vislib/visualizations/point_series.js @@ -55,6 +55,14 @@ export default function PointSeriesFactory(Private) { .attr('class', 'background'); } + addGrid(svg) { + const { width, height } = svg.node().getBBox(); + return svg + .append('g') + .attr('class', 'grid') + .call(this.handler.grid.draw(width, height)); + } + addClipPath(svg) { const { width, height } = svg.node().getBBox(); const startX = 0; @@ -219,6 +227,7 @@ export default function PointSeriesFactory(Private) { .attr('height', height); self.addBackground(svg, width, height); + self.addGrid(svg); self.addClipPath(svg); self.addEvents(svg); self.createEndZones(svg); @@ -227,7 +236,7 @@ export default function PointSeriesFactory(Private) { self.series = []; _.each(self.chartConfig.series, (seriArgs, i) => { if (!seriArgs.show) return; - const SeriClass = seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')]; + const SeriClass = seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')] || seriTypes.line; const series = new SeriClass(self.handler, svg, data.series[i], seriArgs); series.events = self.events; svg.call(series.draw()); diff --git a/src/ui/public/vislib/visualizations/point_series/_point_series.js b/src/ui/public/vislib/visualizations/point_series/_point_series.js index 00ea55ed73585..063c4501de232 100644 --- a/src/ui/public/vislib/visualizations/point_series/_point_series.js +++ b/src/ui/public/vislib/visualizations/point_series/_point_series.js @@ -29,8 +29,8 @@ export default function PointSeriesProvider(Private) { getGroupedCount() { const stacks = []; - return this.baseChart.chartConfig.series.reduce(function (sum, series) { - const valueAxis = series.valueAxis; + return this.baseChart.chartConfig.series.reduce((sum, series) => { + const valueAxis = series.valueAxis || this.baseChart.handler.valueAxes[0].id; const isStacked = series.mode === 'stacked'; const isHistogram = series.type === 'histogram'; if (!isHistogram) return sum; @@ -53,7 +53,7 @@ export default function PointSeriesProvider(Private) { let i = 0; const stacks = []; for (const seri of this.baseChart.chartConfig.series) { - const valueAxis = seri.valueAxis; + const valueAxis = seri.valueAxis || this.baseChart.handler.valueAxes[0].id; const isStacked = seri.mode === 'stacked'; if (!isStacked) { if (seri.data === data) return i; diff --git a/src/ui/public/vislib/visualizations/point_series/area_chart.js b/src/ui/public/vislib/visualizations/point_series/area_chart.js index fc073de6c5ddf..1742dd78e3629 100644 --- a/src/ui/public/vislib/visualizations/point_series/area_chart.js +++ b/src/ui/public/vislib/visualizations/point_series/area_chart.js @@ -79,9 +79,8 @@ export default function AreaChartFactory(Private) { // Append path const path = layer.append('path') .attr('data-label', data.label) - .style('fill', () => { - return color(data.label); - }) + .style('fill', () => color(data.label)) + .style('stroke', () => color(data.label)) .classed('overlap_area', function () { return isOverlapping; }) diff --git a/src/ui/public/vislib/visualizations/point_series/column_chart.js b/src/ui/public/vislib/visualizations/point_series/column_chart.js index 74b74212e554d..54b6db3bb6c58 100644 --- a/src/ui/public/vislib/visualizations/point_series/column_chart.js +++ b/src/ui/public/vislib/visualizations/point_series/column_chart.js @@ -34,7 +34,7 @@ export default function ColumnChartFactory(Private) { const isTooltip = this.handler.visConfig.get('tooltip.show'); const layer = svg.append('g') - .attr('class', 'series') + .attr('class', 'series histogram') .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); const bars = layer.selectAll('rect') @@ -50,9 +50,8 @@ export default function ColumnChartFactory(Private) { .enter() .append('rect') .attr('data-label', data.label) - .attr('fill', () => { - return color(data.label); - }); + .attr('fill', () => color(data.label)) + .attr('stroke', () => color(data.label)); self.updateBars(bars); @@ -117,7 +116,6 @@ export default function ColumnChartFactory(Private) { if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { return yScale(d.y0); } - /*if (!isHorizontal && d.y < 0) return yScale(d.y);*/ return yScale(d.y0 + d.y); } diff --git a/src/ui/public/vislib/visualizations/point_series/line_chart.js b/src/ui/public/vislib/visualizations/point_series/line_chart.js index 9e629c429c126..afdba40ae6340 100644 --- a/src/ui/public/vislib/visualizations/point_series/line_chart.js +++ b/src/ui/public/vislib/visualizations/point_series/line_chart.js @@ -69,8 +69,9 @@ export default function LineChartFactory(Private) { } function cy(d) { + const y0 = d.y0 || 0; const y = d.y || 0; - return yScale(y); + return yScale(y0 + y); } function cColor(d) { @@ -158,7 +159,8 @@ export default function LineChartFactory(Private) { function cy(d) { const y = d.y || 0; - return yScale(y); + const y0 = d.y0 || 0; + return yScale(y0 + y); } line.append('path') diff --git a/src/ui/public/vislib_vis_type/vislib_vis_type.js b/src/ui/public/vislib_vis_type/vislib_vis_type.js index 9b01cb23b6d42..d5272aba2710a 100644 --- a/src/ui/public/vislib_vis_type/vislib_vis_type.js +++ b/src/ui/public/vislib_vis_type/vislib_vis_type.js @@ -4,6 +4,7 @@ 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 'plugins/kbn_vislib_vis_types/controls/point_series'; import VisSchemasProvider from 'ui/vis/schemas'; import VisVisTypeProvider from 'ui/vis/vis_type'; import AggResponsePointSeriesPointSeriesProvider from 'ui/agg_response/point_series/point_series'; @@ -15,6 +16,44 @@ export default function VislibVisTypeFactory(Private) { const pointSeries = Private(AggResponsePointSeriesPointSeriesProvider); const VislibRenderbot = Private(VislibVisTypeVislibRenderbotProvider); + const updateParams = function (params) { + if (!params.seriesParams || !params.seriesParams.length) return; + + const updateIfSet = (from, to, prop, func) => { + if (from[prop]) { + to[prop] = func ? func(from[prop]) : from[prop]; + } + }; + + updateIfSet(params, params.seriesParams[0], 'drawLinesBetweenPoints'); + updateIfSet(params, params.seriesParams[0], 'showCircles'); + updateIfSet(params, params.seriesParams[0], 'radiusRatio'); + updateIfSet(params, params.seriesParams[0], 'interpolate'); + updateIfSet(params, params.seriesParams[0], 'type'); + + if (params.mode) { + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(params.mode); + params.seriesParams[0].mode = stacked ? 'stacked' : 'normal'; + const axisMode = ['stacked', 'overlap'].includes(params.mode) ? 'norlal' : params.mode; + params.valueAxes[0].scale.mode = axisMode; + delete params.mode; + } + + if (params.smoothLines) { + params.seriesParams[0].interpolate = 'cardinal'; + delete params.smoothLines; + } + + updateIfSet(params, params.valueAxes[0].scale, 'setYExtents'); + updateIfSet(params, params.valueAxes[0].scale, 'defaultYExtents'); + + if (params.scale) { + params.valueAxes[0].scale.type = params.scale; + delete params.scale; + } + + updateIfSet(params, params.categoryAxes[0], 'expandLastBucket'); + }; _.class(VislibVisType).inherits(VisType); function VislibVisType(opts = {}) { @@ -28,6 +67,7 @@ export default function VislibVisTypeFactory(Private) { } VislibVisType.prototype.createRenderbot = function (vis, $el, uiState) { + updateParams(vis.params); return new VislibRenderbot(vis, $el, uiState); }; diff --git a/test/functional/apps/visualize/_chart_types.js b/test/functional/apps/visualize/_chart_types.js index 76e265c99fbd1..8bb90b0445292 100644 --- a/test/functional/apps/visualize/_chart_types.js +++ b/test/functional/apps/visualize/_chart_types.js @@ -16,7 +16,7 @@ bdd.describe('visualize app', function describeIndexTests() { bdd.describe('chart types', function indexPatternCreation() { bdd.it('should show the correct chart types', function () { const expectedChartTypes = [ - 'Area chart', 'Data table', 'Heatmap chart', 'Line chart', 'Markdown widget', + 'Area chart', 'Data table', 'Heatmap chart', 'Horizontal bar 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 diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index 2a864126435c8..245419b70f87a 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -52,7 +52,7 @@ bdd.describe('visualize app', function describeIndexTests() { }) .then(function selectField() { PageObjects.common.debug('Field = machine.ram'); - return PageObjects.visualize.selectField('machine.ram'); + return PageObjects.visualize.selectField('machine.ram', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -73,7 +73,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Sum') .then(function selectField() { PageObjects.common.debug('Field = phpmemory'); - return PageObjects.visualize.selectField('phpmemory'); + return PageObjects.visualize.selectField('phpmemory', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -95,7 +95,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Median') .then(function selectField() { PageObjects.common.debug('Field = bytes'); - return PageObjects.visualize.selectField('bytes'); + return PageObjects.visualize.selectField('bytes', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -117,7 +117,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Min') .then(function selectField() { PageObjects.common.debug('Field = @timestamp'); - return PageObjects.visualize.selectField('@timestamp'); + return PageObjects.visualize.selectField('@timestamp', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -138,7 +138,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Max') .then(function selectField() { PageObjects.common.debug('Field = relatedContent.article:modified_time'); - return PageObjects.visualize.selectField('relatedContent.article:modified_time'); + return PageObjects.visualize.selectField('relatedContent.article:modified_time', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -162,7 +162,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Standard Deviation') .then(function selectField() { PageObjects.common.debug('Field = bytes'); - return PageObjects.visualize.selectField('bytes'); + return PageObjects.visualize.selectField('bytes', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -183,7 +183,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Unique Count') .then(function selectField() { PageObjects.common.debug('Field = clientip'); - return PageObjects.visualize.selectField('clientip'); + return PageObjects.visualize.selectField('clientip', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -220,7 +220,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Percentiles') .then(function selectField() { PageObjects.common.debug('Field = machine.ram'); - return PageObjects.visualize.selectField('machine.ram'); + return PageObjects.visualize.selectField('machine.ram', 'metrics'); }) .then(function clickGo() { return PageObjects.visualize.clickGo(); @@ -241,7 +241,7 @@ bdd.describe('visualize app', function describeIndexTests() { return PageObjects.visualize.selectAggregation('Percentile Ranks') .then(function selectField() { PageObjects.common.debug('Field = bytes'); - return PageObjects.visualize.selectField('memory'); + return PageObjects.visualize.selectField('memory', 'metrics'); }) .then(function selectField() { PageObjects.common.debug('Values = 99'); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js new file mode 100644 index 0000000000000..ee19dc45fe5da --- /dev/null +++ b/test/functional/apps/visualize/_point_series_options.js @@ -0,0 +1,192 @@ +import expect from 'expect.js'; + +import { + bdd, +} from '../../../support'; + +import PageObjects from '../../../support/page_objects'; + +bdd.describe('visualize app', function describeIndexTests() { + bdd.before(function () { + const fromTime = '2015-09-19 06:31:44.000'; + const toTime = '2015-09-23 18:31:44.000'; + + PageObjects.common.debug('navigateToApp visualize'); + return PageObjects.common.navigateToUrl('visualize', 'new') + .then(function () { + PageObjects.common.debug('clickLineChart'); + return PageObjects.visualize.clickLineChart(); + }) + .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'); + }) + // add another metrics + .then(function clickAddMetrics() { + PageObjects.common.debug('Add Metric'); + return PageObjects.visualize.clickAddMetric(); + }) + .then(function () { + PageObjects.common.debug('Metric = Value Axis'); + return PageObjects.visualize.clickBucket('Y-Axis'); + }) + .then(function selectAggregation() { + PageObjects.common.debug('Aggregation = Average'); + return PageObjects.visualize.selectAggregation('Average'); + }) + .then(function selectField() { + PageObjects.common.debug('Field = memory'); + return PageObjects.visualize.selectField('machine.ram', 'metrics'); + }) + // go to options page + .then(function gotoAxisOptions() { + PageObjects.common.debug('Going to axis options'); + return PageObjects.visualizeOptions.clickAxisOptions(); + }) + // add another value axis + .then(function addAxis() { + PageObjects.common.debug('adding axis'); + return PageObjects.visualizeOptions.clickAddAxis(); + }) + // set average count to use second value axis + .then(function setAxis() { + return PageObjects.visualizeOptions.toggleCollapsibleTitle('Average machine.ram') + .then(function () { + PageObjects.common.debug('Average memory value axis - ValueAxis-2'); + return PageObjects.visualizeOptions.setSeriesAxis(1, 'ValueAxis-2'); + }); + }) + .then(function clickGo() { + return PageObjects.visualize.clickGo(); + }) + .then(function () { + return PageObjects.header.isGlobalLoadingIndicatorHidden(); + }); + }); + + bdd.describe('secondary value axis', function () { + + bdd.it('should show correct chart, take screenshot', function () { + const expectedChartValues = [ + [ 37, 202, 740, 1437, 1371, 751, 188, 31, 42, 202, 683, + 1361, 1415, 707, 177, 27, 32, 175, 707, 1408, 1355, 726, 201, 29 ], + [ 14018296036, 13284815935, 13198764883, 13093365683, 13067752146, 12976598848, + 13561826081, 14339648875, 14011021362, 12775336396, 13304506791, 12988890398, + 13143466970, 13244378772, 12154757448, 15907286281, 13757317120, 13022240959, + 12807319386, 13375732998, 13190755620, 12627508458, 12731510199, 13153337344 ], + ]; + + // 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(2000) + .then(function () { + return PageObjects.visualize.getLineChartData('fill="#6eadc1"'); + }) + .then(function showData(data) { + PageObjects.common.debug('count data=' + data); + PageObjects.common.debug('data.length=' + data.length); + PageObjects.common.saveScreenshot('Visualize-secondary-value-axis'); + expect(data).to.eql(expectedChartValues[0]); + }) + .then(function () { + return PageObjects.visualize.getLineChartData('fill="#57c17b"', 'ValueAxis-2'); + }) + .then(function showData(data) { + PageObjects.common.debug('average memory data=' + data); + PageObjects.common.debug('data.length=' + data.length); + expect(data).to.eql(expectedChartValues[1]); + }); + }); + + bdd.it('should put secondary axis on the right', function () { + PageObjects.visualizeOptions.getRightValueAxes().then(length => { + expect(length).to.be(1); + }); + }); + }); + + bdd.describe('multiple chart types', function () { + bdd.it('should change average series type to histogram', function () { + return PageObjects.visualizeOptions.toggleCollapsibleTitle('RightAxis-1') + .then(function () { + return PageObjects.visualizeOptions.setSeriesType(1, 'histogram'); + }) + .then(function () { + return PageObjects.visualize.clickGo(); + }) + .then(function () { + return PageObjects.common.sleep(2000); + }) + .then(function checkSeriesTypes() { + PageObjects.visualizeOptions.getHistogramSeries().then(length => { + expect(length).to.be(1); + }); + }); + }); + }); + + bdd.describe('grid lines', function () { + bdd.before(function () { + return PageObjects.visualizeOptions.clickOptions(); + }); + + bdd.it('should show category grid lines', function () { + return PageObjects.visualizeOptions.toggleGridCategoryLines() + .then(function () { + return PageObjects.visualize.clickGo(); + }) + .then(function () { + return PageObjects.common.sleep(2000); + }) + .then(function () { + return PageObjects.visualizeOptions.getGridLines(); + }) + .then(function checkGridLines(gridLines) { + expect(gridLines.length).to.be(9); + gridLines.forEach(gridLine => { + expect(gridLine.y).to.be(0); + }); + + }); + }); + + bdd.it('should show value axis grid lines', function () { + return PageObjects.visualizeOptions.setGridValueAxis('ValueAxis-2') + .then(function () { + return PageObjects.visualizeOptions.toggleGridCategoryLines(); + }) + .then(function () { + return PageObjects.visualize.clickGo(); + }) + .then(function () { + return PageObjects.common.sleep(5000); + }) + .then(function () { + return PageObjects.visualizeOptions.getGridLines(); + }) + .then(function checkGridLines(gridLines) { + expect(gridLines.length).to.be(9); + gridLines.forEach(gridLine => { + expect(gridLine.x).to.be(0); + }); + }); + }); + }); + +}); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index a6cfbdf6c57fc..3333792f39374 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -81,7 +81,7 @@ bdd.describe('visualize app', function describeIndexTests() { // 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) + return PageObjects.common.sleep(50000) .then(function () { return PageObjects.visualize.getBarChartData(); }) diff --git a/test/functional/apps/visualize/index.js b/test/functional/apps/visualize/index.js index e30e83200c543..521d541620b26 100644 --- a/test/functional/apps/visualize/index.js +++ b/test/functional/apps/visualize/index.js @@ -41,4 +41,5 @@ bdd.describe('visualize app', function () { require('./_tile_map'); require('./_vertical_bar_chart'); require('./_heatmap_chart'); + require('./_point_series_options'); }); diff --git a/test/support/page_objects/index.js b/test/support/page_objects/index.js index 50f88057df38f..9566525c4f24e 100644 --- a/test/support/page_objects/index.js +++ b/test/support/page_objects/index.js @@ -7,6 +7,7 @@ import HeaderPage from './header_page'; import SettingsPage from './settings_page'; import ShieldPage from './shield_page'; import VisualizePage from './visualize_page'; +import VisualizePointSeriesOptions from './visualize_point_series_options'; import MonitoringPage from './monitoring_page'; const common = new Common(); @@ -17,6 +18,7 @@ const headerPage = new HeaderPage(); const settingsPage = new SettingsPage(); const shieldPage = new ShieldPage(); const visualizePage = new VisualizePage(); +const visualizePointSeriesOptions = new VisualizePointSeriesOptions(); const monitoringPage = new MonitoringPage(); class PageObjects { @@ -37,6 +39,7 @@ class PageObjects { settingsPage.init(remote); shieldPage.init(remote); visualizePage.init(remote); + visualizePointSeriesOptions.init(remote); monitoringPage.init(remote); } @@ -79,6 +82,10 @@ class PageObjects { return this.assertInitialized() && visualizePage; } + get visualizeOptions() { + return this.assertInitialized() && visualizePointSeriesOptions; + } + get monitoring() { return this.assertInitialized() && monitoringPage; } diff --git a/test/support/page_objects/visualize_page.js b/test/support/page_objects/visualize_page.js index e74fc8d107a97..a06115a9a3d38 100644 --- a/test/support/page_objects/visualize_page.js +++ b/test/support/page_objects/visualize_page.js @@ -9,6 +9,8 @@ export default class VisualizePage { init(remote) { this.remote = remote; + this.xAxisBucket = 'Category Axis'; + this.yAxisBucket = 'Value Axis'; } clickAreaChart() { @@ -39,6 +41,13 @@ export default class VisualizePage { .click(); } + clickAddMetric() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('[group-name="metrics"] .vis-editor-agg-add .vis-editor-agg-wide-btn div.btn') + .click(); + } + clickMetric() { return this.remote .setFindTimeout(defaultFindTimeout) @@ -220,7 +229,7 @@ export default class VisualizePage { selectAggregation(myString) { return this.remote .setFindTimeout(defaultFindTimeout) - .findByCssSelector('option[label="' + myString + '"]') + .findByCssSelector('vis-editor-agg-params:not(.ng-hide) option[label="' + myString + '"]') .click(); } @@ -231,13 +240,13 @@ export default class VisualizePage { .getVisibleText(); } - selectField(fieldValue) { + selectField(fieldValue, groupName = 'buckets') { const self = this; return PageObjects.common.try(function tryingForTime() { return self.remote .setFindTimeout(defaultFindTimeout) // the css below should be more selective - .findByCssSelector('option[label="' + fieldValue + '"]') + .findByCssSelector(`[group-name="${groupName}"] option[label="${fieldValue}"]`) .click(); }); } @@ -474,7 +483,7 @@ export default class VisualizePage { // The current test shows dots, not a line. This function gets the dots and normalizes their height. - getLineChartData(cssPart) { + getLineChartData(cssPart, axis = 'ValueAxis-1') { const self = this.remote; let yAxisLabel = 0; let yAxisHeight; @@ -482,10 +491,10 @@ export default class VisualizePage { // 1). get the maximim chart Y-Axis marker value return this.remote .setFindTimeout(defaultFindTimeout) - .findByCssSelector('div.y-axis-div-wrapper > div > svg > g > g:last-of-type') + .findByCssSelector(`div.y-axis-div-wrapper > div > svg > g.${axis} > g:last-of-type`) .getVisibleText() .then(function (yLabel) { - yAxisLabel = yLabel.replace(',', ''); + yAxisLabel = yLabel.replace(/,/g, ''); PageObjects.common.debug('yAxisLabel = ' + yAxisLabel); return yLabel; }) @@ -505,18 +514,13 @@ export default class VisualizePage { .then(function getChartWrapper() { return self .setFindTimeout(defaultFindTimeout * 2) - .findAllByCssSelector('.chart-wrapper') + .findAllByCssSelector(`.chart-wrapper circle[${cssPart}]`) .then(function (chartTypes) { // 5). for each chart element, find the green circle, then the cy position function getChartType(chart) { return chart - .findByCssSelector(`circle[${cssPart}]`) - .then(function (circleObject) { - // PageObjects.common.debug('circleObject = ' + circleObject + ' yAxisHeight= ' + yAxisHeight + ' yAxisLabel= ' + yAxisLabel); - return circleObject - .getAttribute('cy'); - }) + .getAttribute('cy') .then(function (cy) { // PageObjects.common.debug(' yAxisHeight=' + yAxisHeight + ' yAxisLabel=' + yAxisLabel + ' cy=' + cy + // ' ((yAxisHeight - cy)/yAxisHeight * yAxisLabel)=' + ((yAxisHeight - cy) / yAxisHeight * yAxisLabel)); diff --git a/test/support/page_objects/visualize_point_series_options.js b/test/support/page_objects/visualize_point_series_options.js new file mode 100644 index 0000000000000..881e192da50c0 --- /dev/null +++ b/test/support/page_objects/visualize_point_series_options.js @@ -0,0 +1,171 @@ + +import { + defaultFindTimeout, +} from '../'; + +import PageObjects from './'; + +export default class VisualizePointSeriesOptions { + + init(remote) { + this.remote = remote; + } + + clickOptions() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByPartialLinkText('Panel Settings') + .click(); + } + + clickAxisOptions() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByPartialLinkText('Metrics & Axes') + .click(); + } + + clickAddAxis() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('button[aria-label="Add value axis"]') + .click(); + } + + getValueAxesCount() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.kuiSideBarSection:contains("Value Axes") > .kuiSideBarSection') + .length; + } + + getSeriesCount() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.kuiSideBarSection:contains("Series") > .kuiSideBarSection') + .length; + } + + getRightValueAxes() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.axis-wrapper-right g.axis') + .then(function (data) { + return Promise.all([function () { return data.length; }]); + }); + } + + getHistogramSeries() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.series.histogram') + .then(function (data) { + return Promise.all([function () { return data.length; }]); + }); + } + + getGridLines() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('g.grid > path') + .then(function (data) { + function getGridLine(gridLine) { + return gridLine + .getAttribute('d') + .then(dAttribute => { + const firstPoint = dAttribute.split('L')[0].replace('M', '').split(','); + return { x: parseFloat(firstPoint[0]), y: parseFloat(firstPoint[1]) }; + }); + } + const promises = data.map(getGridLine); + return Promise.all(promises); + }) + .then(function (gridLines) { + return gridLines; + }); + } + + toggleGridCategoryLines() { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('#showCategoryLines') + .click(); + } + + setGridValueAxis(axis) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector(`select#gridAxis option[value="string:${axis}"]`) + .click(); + } + + toggleCollapsibleTitle(title) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findAllByCssSelector('.kuiSideBarCollapsibleTitle .kuiSideBarCollapsibleTitle__text') + .then(sidebarTitles => { + PageObjects.common.debug('found sidebar titles ' + sidebarTitles.length); + function getTitle(titleDiv) { + return titleDiv + .getVisibleText() + .then(titleString => { + PageObjects.common.debug('sidebar title ' + titleString); + if (titleString === title) { + PageObjects.common.debug('clicking sidebar title ' + titleString); + return titleDiv.click(); + } + }); + } + const sidebarTitlePromises = sidebarTitles.map(getTitle); + return Promise.all(sidebarTitlePromises); + }); + } + + setValue(newValue) { + return this.remote + .setFindTimeout(defaultFindTimeout * 2) + .findByCssSelector('button[ng-click="numberListCntr.add()"]') + .click() + .then(() => { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('input[ng-model="numberListCntr.getList()[$index]"]') + .clearValue(); + }) + .then(() => { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector('input[ng-model="numberListCntr.getList()[$index]"]') + .type(newValue); + }); + } + + setValueAxisPosition(axis, position) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector(`select#valueAxisPosition${axis} option[label="${position}"]`) + .click(); + } + + setCategoryAxisPosition(newValue) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector(`select#categoryAxisPosition option[label="${newValue}"]`) + .click(); + } + + setSeriesAxis(series, axis) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector(`select#seriesValueAxis${series} option[value="${axis}"]`) + .click(); + } + + setSeriesType(series, type) { + return this.remote + .setFindTimeout(defaultFindTimeout) + .findByCssSelector(`select#seriesType${series} option[label="${type}"]`) + .click(); + } + +}