59 Commits
Author SHA1 Message Date
Ivan MikheykinandGitHub 742e80d713 Merge pull request #63 from flant/fix_then_for_5_x
Fix then for grafana 5.x before 5.4
2019-03-22 17:58:01 +03:00
Ivan Mikheykin eb6f149f8b version 0.1.1 2019-03-22 17:56:52 +03:00
Ivan Mikheykin d4d297862e Create CHANGELOG.md 2019-03-22 17:55:18 +03:00
Ivan Mikheykin 06cb92bbb8 fix: do not use datasourcePromises for 5.x before 5.4 2019-03-22 17:37:32 +03:00
Ivan Mikheykin 5a758213c6 README: link to Planning issue 2019-03-14 18:00:56 +03:00
Ivan MikheykinandGitHub 20691c9aae Merge pull request #58 from flant/version_0_1_0
Version 0.1.0
2019-03-11 16:36:27 +03:00
Ivan Mikheykin 624be3450d Version 0.1.0 2019-03-11 16:35:16 +03:00
Ivan MikheykinandGitHub 63e36b1656 Merge pull request #57 from flant/readme_influx_mysql_example
README: update info for datasources
2019-03-11 16:18:05 +03:00
Ivan Mikheykin 433a059bcf README: update info for datasources 2019-03-11 16:16:23 +03:00
Ivan MikheykinandGitHub 1862c5ac17 Merge pull request #56 from flant/enable_annotations
Enable annotations
2019-03-11 15:53:53 +03:00
Ivan MikheykinandGitHub 6665b7e18a Merge pull request #39 from vrutkovs/example-dashboard
Create examples folder and add an example dashboard for k8s
2019-03-11 15:53:41 +03:00
Ivan Mikheykin 4994f2f956 feat: Support for annotations: only one time events 2019-03-11 15:38:47 +03:00
Ivan Mikheykin 8d2039bf37 Notifications for grunt watch 2019-03-11 15:35:09 +03:00
Ivan MikheykinandGitHub 12492216d6 Merge pull request #55 from flant/revisit_data_handler
fix: calculate cardWidth over max datapoints for target
2019-03-05 15:50:20 +03:00
Ivan Mikheykin 51623fdc72 fix: calculate cardWidth over max datapoints for target
- correct render of inconsistent timestamps between targets
2019-03-05 15:47:43 +03:00
Ivan MikheykinandGitHub 27b674640b Merge pull request #52 from flant/fix_ie11_removeChild
fix: removeChild error on ie11 fixed with d3 remove
2019-02-26 16:01:20 +03:00
Ivan Mikheykin 803504befd fix: removeChild error on ie11 fixed with d3 remove 2019-02-26 15:53:18 +03:00
Ivan MikheykinandGitHub 570fe11341 Merge pull request #51 from flant/fix_grafana_hangs
Fix grafana hangs and h spacing = 0
2019-02-26 09:45:12 +03:00
Ivan Mikheykin 0537671450 fix: plugin hangs on big values in opacity mode
- legend step is a constant now: 2 pixels
- do not calculate legend if 'No legend'
- remove unnecessary calculations in drawSimple*
2019-02-26 09:36:48 +03:00
Ivan Mikheykin 089f42d940 fix: better description for Rounding option 2019-02-21 20:48:57 +03:00
Ivan Mikheykin 2158753c5e fix: dataWarning div was preventing tooltips
- add ng-if and pointer-events attributs
2019-02-21 20:47:27 +03:00
Ivan Mikheykin dee408e7a3 fix: no more gaps if h spacing is zero
- fixes #31
2019-02-21 20:44:38 +03:00
Ivan MikheykinandGitHub c37d116550 Merge pull request #48 from flant/fix_no_method_remove
fix: no property or method remove on IE11
2019-02-21 15:36:35 +03:00
Ivan Mikheykin a1a06ea78a fix: no property or method remove on IE11
- fixes #40
2019-02-21 15:32:05 +03:00
Vadim Rutkovsky f813f4cea7 Create examples folder and add an example for prometheus + kube-state-metrics 2018-12-27 15:30:16 +01:00
Ivan MikheykinandGitHub 3ffe5554f3 Merge pull request #37 from flant/fix_color_null_error
Fix for null error on undefined values
2018-12-20 15:57:08 +03:00
Ivan Mikheykin e1abad408e Fix for null error on undefined values 2018-12-19 16:08:34 +03:00
Ivan Mikheykin 188d912653 Version 0.0.4 2018-12-13 09:12:16 +03:00
Ivan Mikheykin 8d917d0c1e Version 0.0.4 2018-12-13 09:10:47 +03:00
Ivan MikheykinandGitHub f6973a81b8 Merge pull request #34 from flant/fix_empty_buckets_with_normal_values
Fix display of multivalues buckets as an empty cell
2018-12-11 20:48:38 +03:00
Ivan Mikheykin f13adb369b Fix display of multivalues buckets as an empty cell 2018-12-11 15:58:45 +03:00
Ivan MikheykinandGitHub cb5d353b36 Merge pull request #29 from flant/add_display_options
Display options description in README
2018-11-29 19:20:02 +03:00
Ivan Mikheykin e5249b8852 Display options description in README 2018-11-29 19:19:01 +03:00
Ivan MikheykinandGitHub 8dde6da21a Merge pull request #28 from flant/fix_readme_images
Small fix
2018-11-29 17:09:15 +03:00
Ivan Mikheykin 3c3931b2c0 Small fix 2018-11-29 17:05:54 +03:00
Ivan MikheykinandGitHub 77ab254a5e Merge pull request #27 from flant/fix_readme_images
Images with new improvements in README.md
2018-11-29 17:02:11 +03:00
Ivan Mikheykin 12b974a2bc Images with new improvements in README.md 2018-11-29 17:01:17 +03:00
Ivan MikheykinandGitHub cc4b908afd Merge pull request #24 from flant/update_for_0_0_3
Set version 0.0.3 in plugin.json
2018-11-26 16:33:31 +03:00
Ivan Mikheykin cda8fdfe8d Set version 0.0.3 in plugin.json 2018-11-26 16:32:38 +03:00
Ivan MikheykinandGitHub ec8f8e386a Merge pull request #23 from flant/discrete_preset_of_solarized_colors
Solarized preset for discrete color mode
2018-11-26 16:31:02 +03:00
Ivan Mikheykin 76dc89b2dc Solarized preset for discrete color mode
- add remove all button
2018-11-26 16:28:33 +03:00
Ivan MikheykinandGitHub ca2fcb582a Merge pull request #22 from flant/separate_vertical_and_horizontal_spacing
Separate options for vertical and horizontal spacing of cards
2018-11-26 15:09:17 +03:00
Ivan Mikheykin 54539cb35f Separate options for vertical and horizontal spacing of cards 2018-11-26 15:03:21 +03:00
Ivan MikheykinandGitHub 8e12d35da9 Merge pull request #21 from flant/sort_for_y_axis
Sort options for Y axis
2018-11-26 11:07:16 +03:00
Ivan Mikheykin 34f65b1455 Sort options for Y axis
- sort as on metrics tab
- ascending and descending natural sort (1-2-10)
2018-11-26 11:02:48 +03:00
Ivan MikheykinandGitHub e783d32efa Merge pull request #19 from flant/fix_null_values_as_zero
Fix null values displayed as zeros
2018-11-26 09:32:32 +03:00
Ivan Mikheykin a51f237291 Fix null values displayed as zeros 2018-11-23 18:36:59 +03:00
Ivan MikheykinandGitHub 46ba4be810 Merge pull request #16 from flant/changelog_update
update changelog
2018-11-20 08:49:04 +03:00
Ivan Mikheykin 7ce802f3ef update changelog 2018-10-18 14:27:30 +03:00
Ivan MikheykinandGitHub bccc3320a8 Merge pull request #15 from flant/fix_render
Fix panel rendering timeout
2018-10-18 14:21:46 +03:00
Ivan Mikheykin 5d7cf54348 Fix panel rendering timeout
- minor code cleanups
2018-10-18 14:19:22 +03:00
Ivan MikheykinandGitHub 1f43ffe158 Merge pull request #14 from flant/fix_dark_theme_colors
fix dark theme link in README
2018-10-17 22:39:48 +03:00
Ivan Mikheykin c7882402ce fix dark theme link in README 2018-10-17 22:39:07 +03:00
Ivan MikheykinandGitHub 09cdd2f224 Merge pull request #13 from flant/fix_dark_theme_colors
Support for dark theme
2018-10-17 22:35:57 +03:00
Ivan Mikheykin 0a8463e2ed Support for dark theme
- dark theme colors
- dark theme screenshot
2018-10-17 22:33:27 +03:00
Ivan Mikheykin 4d952a841b Fix version in plugin.json 2018-10-09 04:57:48 +03:00
Ivan MikheykinandGitHub 36e907f8b6 Merge pull request #5 from flant/fix_color_legend
Fix color and opacity legend clip
2018-10-05 12:45:21 +03:00
Ivan Mikheykin af55883905 Fix color and opacity legend clip 2018-10-04 19:21:49 +03:00
Dmitry Shurupov afec01cb93 README text updates 2018-09-21 17:28:37 +07:00
66 changed files with 1933 additions and 349 deletions
+2
View File
@@ -1,2 +1,4 @@
node_modules
.jshintrc
.idea
.sass-cache
+41
View File
@@ -0,0 +1,41 @@
# Changelog
## v0.1.1
- Fix for annotations in grafana 5.x before 5.4
## v0.1.0
- Tested with Grafana 6.0.0
- Tested with InfluxDB and Mysql datasources
- Add initial support for display annotations
- Add example for k8s statuses (thanks, @vrutkovs)
- Fix hanging on big values
- Fix horizontal spacing = 0
- Fix for "Object doesn't support property or method 'remove'"
- Fix card width for targets with different datapoints count
## v0.0.4
- Fix display of multivalues buckets as an empty cell
## v0.0.3
- Add solarized preset
- Reorganize statuses editor for discrete mode
- Separate options for vertical and horizontal spacing for cards
- Add simple sort options for Y axis
- Fix display null values as zero
## v0.0.2
- Install with GF_INSTALL_PLUGINS
- Fix legend overlap
- Fix colors for dark theme
- Fix panel rendering timeout error
## v0.0.1
- First public release
+14 -3
View File
@@ -3,8 +3,18 @@ module.exports = (grunt) => {
grunt.loadNpmTasks('grunt-execute');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-notify');
grunt.initConfig({
notify: {
watch: {
options: {
message: 'grunt watch Complete',
title: 'flant-statusmap-panel rebuilded',
duration: 2
}
}
},
clean: ['dist'],
@@ -17,7 +27,7 @@ module.exports = (grunt) => {
},
pluginDef: {
expand: true,
src: ['plugin.json', 'README.md'],
src: ['plugin.json', 'README.md', 'CHANGELOG.md'],
dest: 'dist'
}
},
@@ -25,7 +35,7 @@ module.exports = (grunt) => {
watch: {
rebuild_all: {
files: ['src/**/*', 'plugin.json'],
tasks: ['default'],
tasks: ['default', 'notify:watch'],
options: {
spawn: false,
livereload: true
@@ -53,7 +63,8 @@ module.exports = (grunt) => {
sass: {
dist: {
files: {
'dist/css/status-heatmap.css': 'src/css/status-heatmap.scss'
'dist/css/statusmap.dark.css': 'src/css/statusmap.dark.scss',
'dist/css/statusmap.light.css': 'src/css/statusmap.light.scss'
}
}
}
+81 -15
View File
@@ -2,22 +2,33 @@
![Statusmap sample panel](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/flant-statusmap-panel.png)
![Statusmap sample panel with dark theme](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/flant-statusmap-panel-dark.png)
## Features
* Grouping values into rows and buckets using legend from query
* User defined color mapping
* Multiple values in bucket can be displayed via tooltip
* Multiple values in bucket are displayed via tooltip
* Increasing rows/buckets' interval for better visual representation
* Representing null values as empty bucket or zero value
:calendar: New features are planned in [#62](https://github.com/flant/grafana-statusmap/issues/62)
### Supported environment
* Prometheus datasource
* Tested with Grafana 5.1.3
* Tested with datasources:
- Prometheus
- InfluxDB
- Mysql
* Tested with Grafana:
- 5.1.3
- 5.4.3
- 6.0.0
## Installation
Plugin can be installed via Git:
Plugin can be installed with GF_INSTALL_PLUGINS="flant-statusmap-panel" or you can use Git and clone this repo:
```
git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/flant-statusmap-panel
@@ -26,15 +37,19 @@ git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/fl
Alternatively, you can download [ZIP archive](https://github.com/flant/grafana-statusmap/archive/master.zip)
of this repo and unpack it into /var/lib/grafana/plugins directory.
## Motivation
We had a desperate need to show a set of timeseries statuses over time period, so we can see
a history of changes for objects' statuses. Since we maintain a lot of Kubernetes clusters
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
see a history of changes for objects' status. Since we maintain a lot of Kubernetes clusters
(and related infrastructure), our main cases for that are visualization of servers & Kubernetes
pods health states as well as HTTP services health checks. We've tried a variety of Grafana
plugins available (they are listed in *Acknowledgements* below) but none of them could provide
the features and visualization really close to what we've been looking for.
_NB: You can find more details about our journey of creating the plugin in
[this post](https://medium.com/flant-com/statusmap-grafana-plugin-to-visualize-status-over-time-fe6ced391853)._
_Objects_ being visualized with this plugin may be different: not only IT components (e.g. server
hosts and Kubernetes pods) but just anything you can imagine like coffee makers on the picture
above. These objects should have _discrete statuses_ which are sets of predefined values, e.g.
@@ -42,9 +57,20 @@ above. These objects should have _discrete statuses_ which are sets of predefine
## Configuration
### Prometheus
### Datasource notes
To work with data from Prometheus you will need to setup discrete statuses of your objects.
To create neat graphs your datasource should return good data. Plugin adjust `$__interval` variable depending on
bucket width in panel options. Your queries should aggregate statuses over `$__interval`.
To make multiple values mode works as expected you should define multiple queries: one query for each possible status.
Plugin doesn't aggregate data in time for now, it only renders input data as buckets. Because of this
data should contain points for each timestamp in time range and equal timestamps for every possible
target (y-axis label). This limitation is addressed by [issue #53](https://github.com/flant/grafana-statusmap/issues/53).
#### Prometheus
To work with data from Prometheus you will need to setup discrete statuses for your objects.
Requirements to store these statuses in metrics are as follows:
* metrics should have two values: `0` and `1`;
* there should be a label with status' value.
@@ -58,8 +84,8 @@ When it's done, you can collect all the data via query, e.g.:
If there was no such status (`<STATUS_VALUE>`) during query's interval, Prometheus would
return nothing. Otherwise, status' value will be returned.
For example, if you have 5 statuses and a metric (`coffee_maker_status`) with 5 allowed
values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using the following rule:
For example, if you have 5 types of statuses and a metric (`coffee_maker_status`) with 5
allowed values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using following rule:
```
- record: coffee_maker_status:discrete
@@ -76,6 +102,28 @@ coffee_maker_status:discrete{status="3"} 1
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
aggregated, so you will get all available statuses of your objects over time.
#### InfluxDB
Choose 'Time series' for 'Format as' and use `GROUP BY ($__interval)` in query. `$tag_<tag name>` can be used in 'Alias by' to define y-axis labels.
#### Mysql
Example query with aggregation over `$__interval` is like this (you need one query for each possible status value):
```
SELECT
$__timeGroupAlias(date_insert,$__interval),
name AS metric,
min(statusi) AS "statusi"
FROM coffee_makers
WHERE
$__timeFilter(date_insert) AND statusi=1
GROUP BY 1,2
ORDER BY $__timeGroup(date_insert,$__interval)
```
`metric` column is used as y-axis label.
### Panel
First of all, an individual query for each possible status' value should be created.
@@ -87,11 +135,20 @@ Then, color mapping for status' values should be defined in __Discrete__ color m
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-mapping.png)
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
![Color mapping empty](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-preset-01.png)
![Color mapping trafic lights](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-preset-02.png)
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
### More options
#### Bucket
![Bucket options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-bucket.png)
__Multiple values__ checkbox specifies how they should be displayed:
@@ -107,8 +164,19 @@ __Null values__ can be treated as empty buckets or displayed with the color of `
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
__Rounding__ may be used to round edges.
![Min width, spacing, rounding](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding.png)
![Min width, spacing, rounding 1](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-01.png)
![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png)
#### Display
![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png)
__Show legend__ checkbox toggles legend at the bottom of the panel.
__Show tooltip__ toggles tooltip display on mouse over buckets.
__Y axis sort__ can be used to sort labels on Y axis. Metrics — sort y labels as they are defined on Metrics tab. a→z and z→a sort labels descending or ascending in a [natural](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) order.
## Development
@@ -135,9 +203,7 @@ The first public release of this plugin has been fully made by [Flant](https://f
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
#### Changelog
##### v0.0.1
- First public release
### CHANGELOG
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
+41
View File
@@ -0,0 +1,41 @@
# Changelog
## v0.1.1
- Fix for annotations in grafana 5.x before 5.4
## v0.1.0
- Tested with Grafana 6.0.0
- Tested with InfluxDB and Mysql datasources
- Add initial support for display annotations
- Add example for k8s statuses (thanks, @vrutkovs)
- Fix hanging on big values
- Fix horizontal spacing = 0
- Fix for "Object doesn't support property or method 'remove'"
- Fix card width for targets with different datapoints count
## v0.0.4
- Fix display of multivalues buckets as an empty cell
## v0.0.3
- Add solarized preset
- Reorganize statuses editor for discrete mode
- Separate options for vertical and horizontal spacing for cards
- Add simple sort options for Y axis
- Fix display null values as zero
## v0.0.2
- Install with GF_INSTALL_PLUGINS
- Fix legend overlap
- Fix colors for dark theme
- Fix panel rendering timeout error
## v0.0.1
- First public release
+142 -31
View File
@@ -2,74 +2,186 @@
![Statusmap sample panel](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/flant-statusmap-panel.png)
![Statusmap sample panel with dark theme](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/flant-statusmap-panel-dark.png)
## Features
* Group values into rows and buckets by query's legend
* Grouping values into rows and buckets using legend from query
* User defined color mapping
* Multiple values in bucket display in tooltip
* Interval shaping to better visual representation
* Represent null values as empty bucket or as zero value
* Multiple values in bucket are displayed via tooltip
* Increasing rows/buckets' interval for better visual representation
* Representing null values as empty bucket or zero value
:calendar: New features are planned in [#62](https://github.com/flant/grafana-statusmap/issues/62)
### Supported environment
* Prometheus datasource
* Tested with Grafana 5.1.3
* Tested with datasources:
- Prometheus
- InfluxDB
- Mysql
* Tested with Grafana:
- 5.1.3
- 5.4.3
- 6.0.0
## Installation
Plugin can be installed with git:
Plugin can be installed with GF_INSTALL_PLUGINS="flant-statusmap-panel" or you can use Git and clone this repo:
```
git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/flant-statusmap-panel
```
Or you can download ZIP archive of this repo and unpack it into /var/lib/grafana/plugins directory.
Alternatively, you can download [ZIP archive](https://github.com/flant/grafana-statusmap/archive/master.zip)
of this repo and unpack it into /var/lib/grafana/plugins directory.
## Motivation
This plugin emerges from our needs to visually represent history of changes for a set of objects
with discrete statuses.
_Objects_ can be hosts, Kubernetes pods or coffee makers and _discrete statuses_ are a set
of predefined values: something like `ok` = 0, `off` = 1, `fail` = 2.
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
see a history of changes for objects' status. Since we maintain a lot of Kubernetes clusters
(and related infrastructure), our main cases for that are visualization of servers & Kubernetes
pods health states as well as HTTP services health checks. We've tried a variety of Grafana
plugins available (they are listed in *Acknowledgements* below) but none of them could provide
the features and visualization really close to what we've been looking for.
_NB: You can find more details about our journey of creating the plugin in
[this post](https://medium.com/flant-com/statusmap-grafana-plugin-to-visualize-status-over-time-fe6ced391853)._
_Objects_ being visualized with this plugin may be different: not only IT components (e.g. server
hosts and Kubernetes pods) but just anything you can imagine like coffee makers on the picture
above. These objects should have _discrete statuses_ which are sets of predefined values, e.g.
`ok` = 0, `off` = 1, `fail` = 2, etc.
## Configuration
Recommended setup is to create query for each possible status value with similar legend:
### Datasource notes
To create neat graphs your datasource should return good data. Plugin adjust `$__interval` variable depending on
bucket width in panel options. Your queries should aggregate statuses over `$__interval`.
To make multiple values mode works as expected you should define multiple queries: one query for each possible status.
Plugin doesn't aggregate data in time for now, it only renders input data as buckets. Because of this
data should contain points for each timestamp in time range and equal timestamps for every possible
target (y-axis label). This limitation is addressed by [issue #53](https://github.com/flant/grafana-statusmap/issues/53).
#### Prometheus
To work with data from Prometheus you will need to setup discrete statuses for your objects.
Requirements to store these statuses in metrics are as follows:
* metrics should have two values: `0` and `1`;
* there should be a label with status' value.
When it's done, you can collect all the data via query, e.g.:
```
(max_over_time(coffee_maker_status{status="<STATUS_VALUE>"}[$__interval]) == 1) * <STATUS_VALUE>
```
If there was no such status (`<STATUS_VALUE>`) during query's interval, Prometheus would
return nothing. Otherwise, status' value will be returned.
For example, if you have 5 types of statuses and a metric (`coffee_maker_status`) with 5
allowed values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using following rule:
```
- record: coffee_maker_status:discrete
expr: |
count_values("status", coffee_maker_status)
```
That's how `coffee_maker_status` metric with value `3` will be transformed into new metric:
```
coffee_maker_status:discrete{status="3"} 1
```
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
aggregated, so you will get all available statuses of your objects over time.
#### InfluxDB
Choose 'Time series' for 'Format as' and use `GROUP BY ($__interval)` in query. `$tag_<tag name>` can be used in 'Alias by' to define y-axis labels.
#### Mysql
Example query with aggregation over `$__interval` is like this (you need one query for each possible status value):
```
SELECT
$__timeGroupAlias(date_insert,$__interval),
name AS metric,
min(statusi) AS "statusi"
FROM coffee_makers
WHERE
$__timeFilter(date_insert) AND statusi=1
GROUP BY 1,2
ORDER BY $__timeGroup(date_insert,$__interval)
```
`metric` column is used as y-axis label.
### Panel
First of all, an individual query for each possible status' value should be created.
Each query should also have similar legend for grouping:
![Query setup](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/queries-example.png)
Next define color mapping for status values in __Discrete__ color mode.
Then, color mapping for status' values should be defined in __Discrete__ color mode:
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-mapping.png)
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
__Spectrum__ and __Opacity__ color modes works as in a [Heatmap](https://grafana.com/plugins/heatmap) plugin.
![Color mapping empty](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-preset-01.png)
![Color mapping trafic lights](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/color-preset-02.png)
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
### More options
#### Bucket
![Bucket options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-bucket.png)
__Multiple values__ check determine multiple values display mode. If check is unset then multiple values
for one bucket treated as error. If check is on then color for bucket determined
by value with least index in color mapping.
__Multiple values__ checkbox specifies how they should be displayed:
* If it's off, multiple values for one bucket are treated as error;
* If it's on, color for such bucket would be determined by the value having least index in color mapping.
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/multiple-values-error.png)
__Null values__ can be treated as empty buckets or displayed as color of 0 value.
__Null values__ can be treated as empty buckets or displayed with the color of `0` value.
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/null-as-empty.png)
__Min width__ and __spacing__ are determine minimal bucket width and spacing between buckets.
__Rounding__ is for round edges.
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
__Rounding__ may be used to round edges.
![Min width, spacing, rounding](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding.png)
![Min width, spacing, rounding 1](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-01.png)
![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png)
#### Display
![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png)
__Show legend__ checkbox toggles legend at the bottom of the panel.
__Show tooltip__ toggles tooltip display on mouse over buckets.
__Y axis sort__ can be used to sort labels on Y axis. Metrics — sort y labels as they are defined on Metrics tab. a→z and z→a sort labels descending or ascending in a [natural](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) order.
## Development
The easy way to test and develop plugin is to run Grafana instance in docker with following command in the directory containing the plugin.
This will expose the local plugin on your machine to the Grafana container.
To test and improve the plugin you can run Grafana instance in Docker using following command (in
the directory containing Statusmap plugin):
```
docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
@@ -78,7 +190,8 @@ docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
grafana/grafana:5.1.3
```
Now run `grunt` to compile dist directory and start changes watcher:
This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile
dist directory and start changes watcher:
```
grunt watch
@@ -86,13 +199,11 @@ grunt watch
## Acknowledgements
Idea of a plugin comes from Dmitry Stolyarov @distol, initial version written by Sergey Gnuskov @gsmetal and final changes made by Ivan Mikheykin @diafour.
The first public release of this plugin has been fully made by [Flant](https://flant.com/) engineers. The whole idea has come from Dmitry Stolyarov ([@distol](https://github.com/distol)), initial version has been written by Sergey Gnuskov ([@gsmetal](https://github.com/gsmetal)) and final changes has been made by Ivan Mikheykin ([@diafour](https://github.com/diafour)).
This plugin is based on a "Heatmap" panel by Grafana and inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
#### Changelog
##### v0.0.1
- First public release
### CHANGELOG
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
+183
View File
@@ -0,0 +1,183 @@
'use strict';
System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_export, _context) {
"use strict";
var d3, $, _, kbn, _createClass, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, AnnotationTooltip;
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
return {
setters: [function (_d) {
d3 = _d.default;
}, function (_jquery) {
$ = _jquery.default;
}, function (_lodash) {
_ = _lodash.default;
}, function (_appCoreUtilsKbn) {
kbn = _appCoreUtilsKbn.default;
}],
execute: function () {
_createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
TOOLTIP_PADDING_X = 30;
TOOLTIP_PADDING_Y = 10;
_export('AnnotationTooltip', AnnotationTooltip = function () {
function AnnotationTooltip(elem, scope) {
_classCallCheck(this, AnnotationTooltip);
this.scope = scope;
this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel;
this.mouseOverAnnotationTick = false;
elem.on("mouseover", this.onMouseOver.bind(this));
elem.on("mouseleave", this.onMouseLeave.bind(this));
}
_createClass(AnnotationTooltip, [{
key: 'onMouseOver',
value: function onMouseOver(e) {
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) {
return;
}
if (!this.tooltip) {
this.add();
this.move(e);
}
}
}, {
key: 'onMouseLeave',
value: function onMouseLeave() {
this.destroy();
}
}, {
key: 'onMouseMove',
value: function onMouseMove(e) {
if (!this.panel.tooltip.show) {
return;
}
this.move(e);
}
}, {
key: 'add',
value: function add() {
this.tooltipBase = d3.select("body").append("div").attr("class", "statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open").style("position", "absolute");
this.tooltip = this.tooltipBase.append("div").attr("class", "drop-content").append("div").append("annotation-tooltip").append("div").attr("class", "graph-annotation");
}
}, {
key: 'destroy',
value: function destroy() {
if (this.tooltip) {
this.tooltip.remove();
}
this.tooltip = null;
if (this.tooltipBase) {
this.tooltipBase.remove();
}
this.tooltipBase = null;
}
}, {
key: 'show',
value: function show(pos) {
if (!this.panel.tooltip.show || !this.tooltip) {
return;
}
// shared tooltip mode
//if (pos.panelRelY) {
// return;
//}
var annoId = d3.select(pos.target).attr('annoId');
if (!annoId) {
this.destroy();
return;
}
var anno = this.panelCtrl.annotations[annoId];
if (!anno) {
this.destroy();
return;
}
var annoTitle = "";
var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
var annoTime = this.dashboard.formatDate(anno.time, tooltipTimeFormat);
var annoText = anno.text;
var annoTags = [];
if (anno.tags) {
annoTags = _.map(anno.tags, function (t) {
return { "text": t, "backColor": "rgb(63, 43, 91)", "borderColor": "rgb(101, 81, 129)" };
});
}
var tooltipHtml = '<div class="graph-annotation__header">\n <span class="graph-annotation__title">' + annoTitle + '</span>\n <span class="graph-annotation__time">' + annoTime + '</span></div>\n <div class="graph-annotation__body">\n <div>' + annoText + '</div>\n ' + _.join(_.map(annoTags, function (t) {
return '<span class="label label-tag small" style="background-color: ' + t.backColor + '; border-color: ' + t.borderColor + '">' + t.text + '</span>';
}), "") + '\n </div>\n <div class="statusmap-histogram"></div>';
this.tooltip.html(tooltipHtml);
this.move(pos);
}
}, {
key: 'move',
value: function move(pos) {
if (!this.tooltipBase) {
return;
}
var elem = $(this.tooltipBase.node())[0];
var tooltipWidth = elem.clientWidth;
var tooltipHeight = elem.clientHeight;
var left = pos.pageX - tooltipWidth / 2;
var top = pos.pageY + TOOLTIP_PADDING_Y;
if (pos.pageX + tooltipWidth / 2 + 10 > window.innerWidth) {
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
}
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
}
return this.tooltipBase.style("left", left + "px").style("top", top + "px");
}
}]);
return AnnotationTooltip;
}());
_export('AnnotationTooltip', AnnotationTooltip);
}
};
});
//# sourceMappingURL=annotations.js.map
+1
View File
File diff suppressed because one or more lines are too long
+41 -45
View File
@@ -3,33 +3,31 @@
System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic/index', 'app/core/core', 'app/core/utils/ticks'], function (_export, _context) {
"use strict";
var angular, _, $, d3, d3ScaleChromatic, contextSrv, tickStep, mod, MIN_LEGEND_STEPS;
var angular, _, $, d3, d3ScaleChromatic, contextSrv, tickStep, mod, LEGEND_STEP_WIDTH;
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
var legendElem = $(elem).find('svg');
var legend = d3.select(legendElem.get(0));
clearLegend(elem);
var legendWidth = Math.floor(legendElem.outerWidth()) - 30;
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
var legendHeight = legendElem.attr("height");
var rangeStep = 1;
if (rangeTo - rangeFrom > legendWidth) {
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
}
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
rangeStep = rangeTo / MIN_LEGEND_STEPS;
}
var rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
// width in pixels in legend space of unit segment in range space
// rangeStep * witdhFactor == width in pixels of one rangeStep
var widthFactor = legendWidth / (rangeTo - rangeFrom);
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
var colorScale = getColorScale(colorScheme, maxValue, minValue);
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
return d * widthFactor;
}).attr("y", 0).attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect")
// translate from range space into pixels
// and shift all rectangles to the right by 10
.attr("x", function (d) {
return d * widthFactor + 10;
}).attr("y", 0)
// rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
return colorScale(d);
});
@@ -41,22 +39,24 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
var legend = d3.select(legendElem.get(0));
clearLegend(elem);
var legendWidth = Math.floor(legendElem.outerWidth()) - 30;
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
var legendHeight = legendElem.attr("height");
var rangeStep = 10;
var rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
// width in pixels in legend space of unit segment in range space
// rangeStep * witdhFactor == width in pixels of one rangeStep
var widthFactor = legendWidth / (rangeTo - rangeFrom);
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
rangeStep = rangeTo / MIN_LEGEND_STEPS;
}
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
var opacityScale = getOpacityScale(options, maxValue, minValue);
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
return d * widthFactor;
}).attr("y", 0).attr("width", rangeStep * widthFactor).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect")
// translate from range space into pixels
// and shift all rectangles to the right by 10
.attr("x", function (d) {
return d * widthFactor + 10;
}).attr("y", 0)
// rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
return opacityScale(d);
});
@@ -80,13 +80,13 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
// calculate max width of tooltip and use it as width for each item
var textWidth = [];
legend.selectAll(".hidden-texts").data(tooltips).enter().append("text").attr("class", "axis tick").attr("font-family", "sans-serif").text(function (d) {
legend.selectAll(".hidden-texts").data(tooltips).enter().append("text").attr("class", "axis tick hidden-texts").attr("font-family", "sans-serif").text(function (d) {
return d;
}).each(function (d, i) {
var thisWidth = this.getBBox().width;
textWidth.push(thisWidth);
this.remove(); // remove them just after displaying them
});
legend.selectAll(".hidden-texts").remove();
var legendWidth = Math.floor(_.min([graphWidth - 30, (_.max(textWidth) + 3) * valuesNumber]));
legendElem.attr("width", legendWidth);
@@ -178,22 +178,18 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
function drawSimpleColorLegend(elem, colorScale) {
var legendElem = $(elem).find('svg');
var legend = d3.select(legendElem.get(0));
clearLegend(elem);
var legendWidth = Math.floor(legendElem.outerWidth());
var legendHeight = legendElem.attr("height");
if (legendWidth) {
var valuesNumber = Math.floor(legendWidth / 2);
var rangeStep = Math.floor(legendWidth / valuesNumber);
var valuesRange = d3.range(0, legendWidth, rangeStep);
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
var legend = d3.select(legendElem.get(0));
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
legendRects.enter().append("rect").attr("x", function (d) {
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
return d;
}).attr("y", 0).attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
}).attr("y", 0).attr("width", LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
return colorScale(d);
});
@@ -202,10 +198,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
function drawSimpleOpacityLegend(elem, options) {
var legendElem = $(elem).find('svg');
var graphElem = $(elem);
var legend = d3.select(legendElem.get(0));
clearLegend(elem);
var legend = d3.select(legendElem.get(0));
var legendWidth = Math.floor(legendElem.outerWidth());
var legendHeight = legendElem.attr("height");
@@ -217,13 +212,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([0, legendWidth]).range([0, 1]);
}
var rangeStep = 10;
var valuesRange = d3.range(0, legendWidth, rangeStep);
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
legendRects.enter().append("rect").attr("x", function (d) {
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
return d;
}).attr("y", 0).attr("width", rangeStep).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
}).attr("y", 0).attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
return legendOpacityScale(d);
});
}
@@ -329,11 +322,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
}],
execute: function () {
mod = angular.module('grafana.directives');
MIN_LEGEND_STEPS = 10;
LEGEND_STEP_WIDTH = 2;
/**
* Color legend for heatmap editor.
* Bigger color legend for opacity and spectrum modes editor.
*/
mod.directive('optionsColorLegend', function () {
return {
@@ -367,7 +360,7 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
});
/**
* Heatmap legend with scale values.
* Graph legend with values.
*/
mod.directive('statusHeatmapLegend', function () {
return {
@@ -384,6 +377,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
function render() {
clearLegend(elem);
if (!ctrl.panel.legend.show) {
return;
}
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
var rangeFrom = ctrl.cardsData.minValue;
var rangeTo = ctrl.cardsData.maxValue;
+1
View File
File diff suppressed because one or more lines are too long
+27 -3
View File
@@ -101,9 +101,33 @@ System.register(["lodash"], function (_export, _context) {
value: function getBucketColor(values) {
var thresholds = this.panel.color.thresholds;
if (!values || values.length == 0) {
// treat as null value
return this.getMatchedThreshold(null).color;
}
if (values.length == 1) {
var threshold = this.getMatchedThreshold(values[0]);
if (!threshold || !threshold.color || threshold.color == "") {
return 'rgba(0,0,0,1)';
} else {
return threshold.color;
}
}
var isAllValuesNulls = true;
for (var j = 0; j < values.length; j++) {
if (values[j] != null) {
isAllValuesNulls = false;
}
}
if (isAllValuesNulls) {
return this.getMatchedThreshold(null).color;
}
for (var i = 0; i < thresholds.length; i++) {
for (var j = 0; j < values.length; j++) {
if (values[j] == thresholds[i].value) {
for (var _j = 0; _j < values.length; _j++) {
if (values[_j] == thresholds[i].value) {
return this.getDiscreteColor(i);
}
}
@@ -135,7 +159,7 @@ System.register(["lodash"], function (_export, _context) {
key: "getMatchedThreshold",
value: function getMatchedThreshold(value) {
if (value == null) {
if (this.panel.color.nullPointMode == 'as empty') {
if (this.panel.nullPointMode == 'as empty') {
// FIXME: make this explicit for user
// Right now this color never used because null as empty handles in getCardOpacity method.
return {
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"mappings": "AAGA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAEjB,uDAAyB;IACzB,gBAAgB,EAAE,KAAK;IACvB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,IAAI;EAGjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO;EAKnB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,uBAAwB;EACtB,WAAW,EAAE,MAAM;EACnB,SAAS,EAzCI,IAAI;EA0CjB,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;;AAGhB,8BAA+B;EAC7B,IAAI,EAAE,OAAO;;AAIb,8BAAK;EACH,MAAM,EAAE,OAAe;EACvB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAGlC,8BAA+B;EAE7B,MAAM,EAAE,MAAM;EAGd,kCAAI;IAGF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,IAAI;EAGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO;EAGjB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO",
"sources": ["../../src/css/status-heatmap.scss"],
"names": [],
"file": "status-heatmap.css"
}
+68
View File
@@ -0,0 +1,68 @@
.status-heatmap-canvas-wrapper {
cursor: crosshair; }
.status-heatmap-canvas-wrapper .datapoints-warning {
pointer-events: none; }
.status-heatmap-canvas-wrapper .datapoints-warning span {
background-color: #333333;
color: #d8d9da;
padding: 1px; }
.status-heatmap-panel {
position: relative; }
.status-heatmap-panel .axis .tick text {
fill: #d8d9da;
color: #d8d9da;
font-size: 11px; }
.status-heatmap-panel .axis .tick line {
opacity: 0.4;
stroke: #8e8e8e; }
.status-heatmap-panel svg {
pointer-events: none; }
.status-heatmap-panel svg rect {
pointer-events: visiblePainted; }
.statusmap-tooltip {
white-space: nowrap;
font-size: 12px;
background-color: #141414;
color: #d8d9da; }
.statusmap-tooltip .discrete-item {
color: #52545c;
padding: 1px;
font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; }
.statusmap-histogram rect {
fill: #8e8e8e; }
.status-heatmap-crosshair line {
stroke: #9f3023;
stroke-width: 1; }
.status-heatmap-selection {
stroke-width: 1;
fill: rgba(102, 102, 102, 0.4);
stroke: rgba(102, 102, 102, 0.8); }
.status-heatmap-legend-wrapper {
margin: 0 10px; }
.status-heatmap-legend-wrapper svg {
height: 24px;
float: left;
white-space: nowrap; }
.status-heatmap-legend-wrapper .status-heatmap-color-legend {
margin-top: 4px; }
.status-heatmap-legend-wrapper .status-heatmap-legend-values {
display: inline-block; }
.status-heatmap-legend-wrapper .axis .tick text {
fill: #d8d9da;
color: #d8d9da;
font-size: 11px; }
.status-heatmap-legend-wrapper .axis .tick line {
opacity: 0.4;
stroke: #8e8e8e; }
.status-heatmap-legend-wrapper .axis .tick .domain {
opacity: 0.4;
stroke: #8e8e8e; }
/*# sourceMappingURL=statusmap.dark.css.map */
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAGjB,kDAAoB;IAClB,cAAc,EAAE,IAAI;EAGtB,uDAAyB;IACzB,gBAAgB,ECNL,OAAO;IDOlB,KAAK,ECVM,OAAO;IDWlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EE5CI,IAAI;EF6CjB,gBAAgB,EC1CC,OAAO;ED2CxB,KAAK,EC7CM,OAAO;ED+ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,ECvDY,OAAO;;AD2DvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,EC7FG,OAAO;ID8Fd,KAAK,EC9FE,OAAO;ID+Fd,SAAS,EE/FE,IAAI;EFkGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO;EDsGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECxGM,OAAO",
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.dark.scss","../../src/css/_variables.scss"],
"names": [],
"file": "statusmap.dark.css"
}
@@ -1,7 +1,10 @@
.status-heatmap-canvas-wrapper {
cursor: crosshair; }
.status-heatmap-canvas-wrapper .datapoints-warning {
pointer-events: none; }
.status-heatmap-canvas-wrapper .datapoints-warning span {
background-color: white;
background-color: #e9edf2;
color: #52545c;
padding: 1px; }
.status-heatmap-panel {
@@ -18,17 +21,22 @@
.status-heatmap-panel svg rect {
pointer-events: visiblePainted; }
.status-heatmap-tooltip {
.statusmap-tooltip {
white-space: nowrap;
font-size: 12px;
background-color: #dde4ed;
color: #52545c; }
.statusmap-tooltip .discrete-item {
color: #52545c;
padding: 1px;
font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; }
.status-heatmap-histogram rect {
.statusmap-histogram rect {
fill: #767980; }
.status-heatmap-crosshair line {
stroke: #b30000;
stroke: #9e2f22;
stroke-width: 1; }
.status-heatmap-selection {
@@ -57,4 +65,4 @@
opacity: 0.4;
stroke: #767980; }
/*# sourceMappingURL=status-heatmap.css.map */
/*# sourceMappingURL=statusmap.light.css.map */
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAGjB,kDAAoB;IAClB,cAAc,EAAE,IAAI;EAGtB,uDAAyB;IACzB,gBAAgB,ECNL,OAAO;IDOlB,KAAK,ECVM,OAAO;IDWlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EE5CI,IAAI;EF6CjB,gBAAgB,EC1CC,OAAO;ED2CxB,KAAK,EC7CM,OAAO;ED+ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,ECvDY,OAAO;;AD2DvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,EC7FG,OAAO;ID8Fd,KAAK,EC9FE,OAAO;ID+Fd,SAAS,EE/FE,IAAI;EFkGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO;EDsGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECxGM,OAAO",
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.light.scss","../../src/css/_variables.scss"],
"names": [],
"file": "statusmap.light.css"
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
</div>
+11 -3
View File
@@ -1,14 +1,22 @@
'use strict';
System.register(['./color_legend', './status_heatmap_ctrl'], function (_export, _context) {
System.register(['app/plugins/sdk', './status_heatmap_ctrl'], function (_export, _context) {
"use strict";
var StatusHeatmapCtrl;
var loadPluginCss, StatusHeatmapCtrl;
return {
setters: [function (_color_legend) {}, function (_status_heatmap_ctrl) {
setters: [function (_appPluginsSdk) {
loadPluginCss = _appPluginsSdk.loadPluginCss;
}, function (_status_heatmap_ctrl) {
StatusHeatmapCtrl = _status_heatmap_ctrl.StatusHeatmapCtrl;
}],
execute: function () {
loadPluginCss({
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
});
_export('PanelCtrl', StatusHeatmapCtrl);
}
};
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../src/module.js"],"names":["loadPluginCss","StatusHeatmapCtrl","dark","light"],"mappings":";;;;;;;;AAAQA,mB,kBAAAA,a;;AACCC,uB,wBAAAA,iB;;;;AAETD,oBAAc;AACZE,cAAM,sDADM;AAEZC,eAAO;AAFK,OAAd;;2BAMEF,iB","file":"module.js","sourcesContent":["import {loadPluginCss} from 'app/plugins/sdk';\nimport { StatusHeatmapCtrl } from './status_heatmap_ctrl';\n\nloadPluginCss({\n dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',\n light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'\n});\n\nexport {\n StatusHeatmapCtrl as PanelCtrl\n};\n"]}
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../src/options_editor.js"],"names":["statusHeatmapOptionsEditor","restrict","scope","templateUrl","controller","StatusHeatmapOptionsEditorCtrl","kbn","$scope","editor","panelCtrl","ctrl","panel","unitFormats","getUnitFormats","render","subItem","data","unitFormat","value"],"mappings":";;;;;;;;;;;;;AAkBO,WAASA,0BAAT,GAAsC;AAC3C;;AACA,WAAO;AACLC,gBAAU,GADL;AAELC,aAAO,IAFF;AAGLC,mBAAa,mEAHR;AAILC,kBAAYC;AAJP,KAAP;AAMD;;wCAReL,0B;;;;AAlBTM,S;;;;;;;;;;;;;;;;;;;;;gDAEMD,8B;AACX,gDAAYE,MAAZ,EAAoB;AAAA;;AAClBA,iBAAOC,MAAP,GAAgB,IAAhB;AACA,eAAKC,SAAL,GAAiBF,OAAOG,IAAxB;AACA,eAAKC,KAAL,GAAa,KAAKF,SAAL,CAAeE,KAA5B;AACA,eAAKC,WAAL,GAAmBN,IAAIO,cAAJ,EAAnB;;AAEA,eAAKJ,SAAL,CAAeK,MAAf;AACD;;;;wCAEaC,O,EAAS;AACrB,iBAAKJ,KAAL,CAAWK,IAAX,CAAgBC,UAAhB,GAA6BF,QAAQG,KAArC;AACA,iBAAKT,SAAL,CAAeK,MAAf;AACD","file":"options_editor.js","sourcesContent":["import kbn from 'app/core/utils/kbn';\n\nexport class StatusHeatmapOptionsEditorCtrl {\n constructor($scope) {\n $scope.editor = this;\n this.panelCtrl = $scope.ctrl;\n this.panel = this.panelCtrl.panel;\n this.unitFormats = kbn.getUnitFormats();\n\n this.panelCtrl.render();\n }\n\n setUnitFormat(subItem) {\n this.panel.data.unitFormat = subItem.value;\n this.panelCtrl.render();\n }\n}\n\nexport function statusHeatmapOptionsEditor() {\n 'use strict';\n return {\n restrict: 'E',\n scope: true,\n templateUrl: 'public/plugins/flant-statusmap-panel/partials/options_editor.html',\n controller: StatusHeatmapOptionsEditorCtrl,\n };\n}\n"]}
+78 -35
View File
@@ -55,40 +55,70 @@
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
</div>
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
<label class="gf-form-label width-2">{{ $index }}</label>
<label class="gf-form-label width-9" ng-if="ctrl.panel.useMax">If bucket has value</label>
<label class="gf-form-label width-9" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
<label class="gf-form-label">
fill
</label>
<label class="gf-form-label">
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
</label>
<label class="gf-form-label">set tooltip</label>
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.onEditorRemoveThreshold($index)">
<i class="fa fa-trash"/>
</a>
</label>
<div class="gf-form" ng-show="ctrl.panel.color.thresholds.length == 0">
<label class="gf-form-label width-2">0</label>
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</label>
</div>
<div class="gf-form">
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
Add trafic lights
</a>
</label>
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
Add new status
</a>
</label>
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
<div class="gf-form">
<label class="gf-form-label width-2">{{ $index }}</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
<label class="gf-form-label query-keyword">
fill
</label>
<label class="gf-form-label">
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
</label>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">set tooltip</label>
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
<i class="fa fa-trash"/>
</a>
</label>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form"></div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()">
<i class="fa fa-plus"></i> Add new status
</button>
</div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()">
<i class="fa fa-minus"></i> Remove all
</button>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.panel.color.thresholds.length == 0">
<div class="gf-form"></div>
<div class="gf-form">
<label class="gf-form-label">Presets</label>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()">
Red-Yellow-Green
</button>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddSolarized()">
Solarized
</button>
</div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
@@ -118,7 +148,16 @@
label="Show tooltip"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Y axis sort</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-8"
ng-model="ctrl.panel.yAxisSort"
ng-options="f for f in ['metrics', 'a → z', 'z → a']"
ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Bucket</h5>
@@ -142,12 +181,16 @@
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
<label class="gf-form-label width-9">V spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards vertical spacing in pixels'" ng-model="ctrl.panel.cards.cardVSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">H spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Rounding</label>
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Angles rounding in percent'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
</div>
+4 -3
View File
@@ -20,12 +20,13 @@
],
"screenshots": [
{"name": "Panel", "path": "img/flant-statusmap-panel.png"},
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
{"name": "Panel with dark theme", "path": "img/flant-statusmap-panel-dark.png"},
{"name": "Color mapping", "path": "img/color-mapping.png"},
{"name": "Queries example", "path": "img/queries-example.png"}
],
"version": "0.0.1",
"updated": "2018-09-17"
"version": "0.1.1",
"updated": "2019-03-22"
},
"dependencies": {
+93 -18
View File
@@ -1,9 +1,9 @@
'use strict';
System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/core', 'app/core/utils/ticks', 'd3', './libs/d3-scale-chromatic/index', './tooltip'], function (_export, _context) {
System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/core', 'app/core/utils/ticks', 'd3', './libs/d3-scale-chromatic/index', './tooltip', './annotations'], function (_export, _context) {
"use strict";
var _, $, moment, kbn, appEvents, contextSrv, tickStep, getScaledDecimals, getFlotTickSize, d3, d3ScaleChromatic, StatusHeatmapTooltip, MIN_CARD_SIZE, CARD_SPACING, CARD_ROUND, DATA_RANGE_WIDING_FACTOR, DEFAULT_X_TICK_SIZE_PX, DEFAULT_Y_TICK_SIZE_PX, X_AXIS_TICK_PADDING, Y_AXIS_TICK_PADDING, MIN_SELECTION_WIDTH;
var _, $, moment, kbn, appEvents, contextSrv, tickStep, getScaledDecimals, getFlotTickSize, d3, d3ScaleChromatic, StatusHeatmapTooltip, AnnotationTooltip, MIN_CARD_SIZE, CARD_H_SPACING, CARD_V_SPACING, CARD_ROUND, DATA_RANGE_WIDING_FACTOR, DEFAULT_X_TICK_SIZE_PX, DEFAULT_Y_TICK_SIZE_PX, X_AXIS_TICK_PADDING, Y_AXIS_TICK_PADDING, MIN_SELECTION_WIDTH;
function link(scope, elem, attrs, ctrl) {
var data = void 0,
@@ -15,6 +15,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
// $heatmap is JQuery object, but heatmap is D3
var $heatmap = elem.find('.status-heatmap-panel');
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
var annotationTooltip = new AnnotationTooltip($heatmap, scope);
var width = void 0,
height = void 0,
@@ -26,7 +27,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
chartBottom = void 0,
yAxisWidth = void 0,
xAxisHeight = void 0,
cardSpacing = void 0,
cardVSpacing = void 0,
cardHSpacing = void 0,
cardRound = void 0,
cardWidth = void 0,
cardHeight = void 0,
@@ -77,7 +79,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
return text.getBBox().width;
}));
return max_text_width;
return Math.ceil(max_text_width);
}
function getXAxisHeight(elem) {
@@ -121,9 +123,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
function getYScale(ticks) {
var range = [];
var step = chartHeight / ticks.length;
range.push(chartHeight);
// svg has y=0 on the top, so top card should have a minimal value in range
range.push(step);
for (var i = 1; i < ticks.length; i++) {
range.push(chartHeight - step * i);
range.push(step * (i + 1));
}
return d3.scaleOrdinal().domain(ticks).range(range);
}
@@ -132,9 +135,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
function getYAxisScale(ticks) {
var range = [];
var step = chartHeight / ticks.length;
range.push(chartHeight - yOffset);
// svg has y=0 on the top, so top tick should have a minimal value in range
range.push(yOffset);
for (var i = 1; i < ticks.length; i++) {
range.push(chartHeight - step * i - yOffset);
range.push(step * i + yOffset);
}
return d3.scaleOrdinal().domain(ticks).range(range);
}
@@ -149,6 +153,16 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
ticks = [''];
}
if (panel.yAxisSort == 'a → z') {
ticks.sort(function (a, b) {
return a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true });
});
} else if (panel.yAxisSort == 'z → a') {
ticks.sort(function (b, a) {
return a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true });
});
}
var yAxisScale = getYAxisScale(ticks);
scope.yScale = yScale = getYScale(ticks);
@@ -216,12 +230,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
chartTop = margin.top;
chartBottom = chartTop + chartHeight;
cardSpacing = panel.cards.cardSpacing !== null ? panel.cards.cardSpacing : CARD_SPACING;
cardHSpacing = panel.cards.cardHSpacing !== null ? panel.cards.cardHSpacing : CARD_H_SPACING;
cardVSpacing = panel.cards.cardVSpacing !== null ? panel.cards.cardVSpacing : CARD_V_SPACING;
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
// calculate yOffset for YAxis
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
yOffset = cardHeight / 2;
addYAxis();
@@ -229,9 +244,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
chartWidth = width - yAxisWidth - margin.right;
// TODO allow per-y cardWidth!
// we need to fill chartWidth with xBucketSize cards.
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
cardWidth = xGridSize - cardSpacing;
cardWidth = xGridSize - cardHSpacing;
addXAxis();
xAxisHeight = getXAxisHeight(heatmap);
@@ -274,6 +290,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
resetCardHighLight(event);
});
_renderAnnotations();
ctrl.events.emit('render-complete', {
"chartWidth": chartWidth
});
@@ -321,7 +339,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
var cx = xScale(d.x);
if (cx - cardWidth / 2 < 0) {
x = yAxisWidth + cardSpacing / 2;
x = yAxisWidth + cardHSpacing / 2;
} else {
x = yAxisWidth + cx - cardWidth / 2;
}
@@ -337,11 +355,11 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
if (cx < cardWidth / 2) {
// Center should not exceed half of card.
// Cut card to the left to prevent overlay of y axis.
var cutted_width = cx - cardSpacing / 2 + cardWidth / 2;
var cutted_width = cx - cardHSpacing / 2 + cardWidth / 2;
w = cutted_width > 0 ? cutted_width : 0;
} else if (chartWidth - cx < cardWidth / 2) {
// Cut card to the right to prevent overlay of right graph edge.
w = cardWidth / 2 + (chartWidth - cx - cardSpacing / 2);
w = cardWidth / 2 + (chartWidth - cx - cardHSpacing / 2);
} else {
w = cardWidth;
}
@@ -349,21 +367,25 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
// Card width should be MIN_CARD_SIZE at least
w = Math.max(w, MIN_CARD_SIZE);
if (cardHSpacing == 0) {
w = w + 1;
}
return w;
}
function getCardY(d) {
return yScale(d.y) + chartTop - cardHeight - cardSpacing / 2;
return yScale(d.y) + chartTop - cardHeight - cardVSpacing / 2;
}
function getCardHeight(d) {
var ys = yScale(d.y);
var y = ys + chartTop - cardHeight - cardSpacing / 2;
var y = ys + chartTop - cardHeight - cardVSpacing / 2;
var h = cardHeight;
// Cut card height to prevent overlay
if (y < chartTop) {
h = ys - cardSpacing / 2;
h = ys - cardVSpacing / 2;
} else if (ys > chartBottom) {
h = chartBottom - y;
} else if (y + cardHeight > chartBottom) {
@@ -375,6 +397,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
// Card height should be MIN_CARD_SIZE at least
h = Math.max(h, MIN_CARD_SIZE);
if (cardVSpacing == 0) {
h = h + 1;
}
return h;
}
@@ -452,6 +478,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
function onMouseLeave() {
appEvents.emit('graph-hover-clear');
clearCrosshair();
//annotationTooltip.destroy();
}
function onMouseMove(event) {
@@ -463,6 +490,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
// Clear crosshair and tooltip
clearCrosshair();
tooltip.destroy();
annotationTooltip.destroy();
selection.x2 = limitSelection(event.offsetX);
drawSelection(selection.x1, selection.x2);
@@ -470,6 +498,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
emitGraphHoverEvet(event);
drawCrosshair(event.offsetX);
tooltip.show(event); //, data);
annotationTooltip.show(event);
}
}
@@ -572,6 +601,49 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
$heatmap.on("mousedown", onMouseDown);
$heatmap.on("mousemove", onMouseMove);
$heatmap.on("mouseleave", onMouseLeave);
function _renderAnnotations() {
if (!ctrl.annotations || ctrl.annotations.length == 0) {
return;
}
if (!heatmap) {
return;
}
var annoData = _.map(ctrl.annotations, function (d, i) {
return { "x": Math.floor(yAxisWidth + xScale(d.time)), "id": i, "anno": d.source };
});
var anno = heatmap.append("g").attr("class", "statusmap-annotations").attr("transform", "translate(0.5,0)").selectAll(".statusmap-annotations").data(annoData).enter().append("g");
anno.append("line")
//.attr("class", "statusmap-annotation-tick")
.attr("x1", function (d) {
return d.x;
}).attr("y1", chartTop).attr("x2", function (d) {
return d.x;
}).attr("y2", chartBottom).style("stroke", function (d) {
return d.anno.iconColor;
}).style("stroke-width", 1).style("stroke-dasharray", "3,3");
anno.append("polygon").attr("points", function (d) {
return [[d.x, chartBottom + 1], [d.x - 5, chartBottom + 6], [d.x + 5, chartBottom + 6]].join(" ");
}).style("stroke-width", 0).style("fill", function (d) {
return d.anno.iconColor;
});
// Polygons didn't fire mouseevents
anno.append("rect").attr("x", function (d) {
return d.x - 5;
}).attr("width", 10).attr("y", chartBottom + 1).attr("height", 5).attr("class", "statusmap-annotation-tick").attr("annoId", function (d) {
return d.id;
}).style("opacity", 0);
var $ticks = $heatmap.find(".statusmap-annotation-tick");
$ticks.on("mouseenter", function (event) {
annotationTooltip.mouseOverAnnotationTick = true;
}).on("mouseleave", function (event) {
annotationTooltip.mouseOverAnnotationTick = false;
});
}
}
_export('default', link);
@@ -622,10 +694,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
}, function (_tooltip) {
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
}, function (_annotations) {
AnnotationTooltip = _annotations.AnnotationTooltip;
}],
execute: function () {
MIN_CARD_SIZE = 5;
CARD_SPACING = 2;
CARD_H_SPACING = 2;
CARD_V_SPACING = 2;
CARD_ROUND = 0;
DATA_RANGE_WIDING_FACTOR = 1.2;
DEFAULT_X_TICK_SIZE_PX = 100;
+1
View File
File diff suppressed because one or more lines are too long
+127 -30
View File
@@ -1,9 +1,9 @@
'use strict';
System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/kbn', './rendering', './options_editor', './color_mode_discrete', './css/status-heatmap.css!'], function (_export, _context) {
System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/kbn', './color_legend', './rendering', './options_editor', './color_mode_discrete'], function (_export, _context) {
"use strict";
var MetricsPanelCtrl, _, contextSrv, kbn, rendering, statusHeatmapOptionsEditor, ColorModeDiscrete, CANVAS, SVG, VALUE_INDEX, TIME_INDEX, panelDefaults, renderer, colorSchemes, colorModes, opacityScales, StatusHeatmapCtrl;
var MetricsPanelCtrl, _, contextSrv, kbn, rendering, statusHeatmapOptionsEditor, ColorModeDiscrete, _get, CANVAS, SVG, VALUE_INDEX, TIME_INDEX, panelDefaults, renderer, colorSchemes, colorModes, opacityScales, StatusHeatmapCtrl;
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
@@ -44,14 +44,39 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
contextSrv = _appCoreCore.contextSrv;
}, function (_appCoreUtilsKbn) {
kbn = _appCoreUtilsKbn.default;
}, function (_rendering) {
}, function (_color_legend) {}, function (_rendering) {
rendering = _rendering.default;
}, function (_options_editor) {
statusHeatmapOptionsEditor = _options_editor.statusHeatmapOptionsEditor;
}, function (_color_mode_discrete) {
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
}, function (_cssStatusHeatmapCss) {}],
}],
execute: function () {
_get = function get(object, property, receiver) {
if (object === null) object = Function.prototype;
var desc = Object.getOwnPropertyDescriptor(object, property);
if (desc === undefined) {
var parent = Object.getPrototypeOf(object);
if (parent === null) {
return undefined;
} else {
return get(parent, property, receiver);
}
} else if ("value" in desc) {
return desc.value;
} else {
var getter = desc.get;
if (getter === undefined) {
return undefined;
}
return getter.call(receiver);
}
};
CANVAS = 'CANVAS';
SVG = 'SVG';
VALUE_INDEX = 0;
@@ -71,7 +96,8 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
},
cards: {
cardMinWidth: 5,
cardSpacing: 2,
cardVSpacing: 2,
cardHSpacing: 2,
cardRound: null
},
xAxis: {
@@ -97,6 +123,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
},
// how null points should be handled
nullPointMode: 'as empty',
yAxisSort: 'metrics',
highlightCards: true,
useMax: true
};
@@ -109,20 +136,22 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
{ name: 'Blues', value: 'interpolateBlues', invert: 'dark' }, { name: 'Greens', value: 'interpolateGreens', invert: 'dark' }, { name: 'Greys', value: 'interpolateGreys', invert: 'dark' }, { name: 'Oranges', value: 'interpolateOranges', invert: 'dark' }, { name: 'Purples', value: 'interpolatePurples', invert: 'dark' }, { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
// Sequential (Multi-Hue)
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, { name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' }, { name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' }, { name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' }, { name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' }, { name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' }, { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }];
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, { name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' }, { name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' }, { name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' }, { name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' }, { name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' }, { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }];
colorModes = ['opacity', 'spectrum', 'discrete'];
opacityScales = ['linear', 'sqrt'];
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv) {
/** @ngInject */
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
_classCallCheck(this, StatusHeatmapCtrl);
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
_this.onRenderComplete = function (data) {
_this.graph.chartWidth = data.chartWidth;
_this.renderingCompleted();
};
_this.calculateInterval = function () {
@@ -131,7 +160,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
var chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
var minCardWidth = _this.panel.cards.cardMinWidth;
var minSpacing = _this.panel.cards.cardSpacing;
var minSpacing = _this.panel.cards.cardHSpacing;
var maxCardsCount = Math.ceil((chartWidth - minCardWidth) / (minCardWidth + minSpacing));
var intervalMs = void 0;
@@ -166,11 +195,49 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
_this.interval = kbn.secondsToHms(intervalMs / 1000);
};
_this.issueQueries = function (datasource) {
_this.annotationsPromise = _this.annotationsSrv.getAnnotations({
dashboard: _this.dashboard,
panel: _this.panel,
range: _this.range
});
/* Wait for annotationSrv requests to get datasources to
* resolve before issuing queries. This allows the annotations
* service to fire annotations queries before graph queries
* (but not wait for completion). This resolves
* issue 11806.
*/
// 5.x before 5.4 doesn't have dataPromises
if ("undefined" !== typeof _this.annotationsSrv.datasourcePromises) {
return _this.annotationsSrv.datasourcePromises.then(function (r) {
return _get(StatusHeatmapCtrl.prototype.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl.prototype), 'issueQueries', _this).call(_this, datasource);
});
} else {
return _get(StatusHeatmapCtrl.prototype.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl.prototype), 'issueQueries', _this).call(_this, datasource);
}
};
_this.onDataReceived = function (dataList) {
_this.data = dataList;
_this.cardsData = _this.convertToCards(_this.data);
_this.render();
_this.annotationsPromise.then(function (result) {
_this.loading = false;
//this.alertState = result.alertState;
if (result.annotations && result.annotations.length > 0) {
_this.annotations = result.annotations;
} else {
_this.annotations = null;
}
_this.render();
}, function () {
_this.loading = false;
_this.annotations = null;
_this.render();
});
//this.render();
};
_this.onInitEditMode = function () {
@@ -206,6 +273,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
_this.onDataError = function () {
_this.data = [];
_this.annotations = [];
_this.render();
};
@@ -223,6 +291,11 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
_this.render();
};
_this.onEditorRemoveThresholds = function () {
_this.panel.color.thresholds = [];
_this.render();
};
_this.onEditorAddThreeLights = function () {
_this.panel.color.thresholds.push({ color: "red", value: 2, tooltip: "error" });
_this.panel.color.thresholds.push({ color: "yellow", value: 1, tooltip: "warning" });
@@ -230,6 +303,18 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
_this.render();
};
_this.onEditorAddSolarized = function () {
_this.panel.color.thresholds.push({ color: "#b58900", value: 0, tooltip: "yellow" });
_this.panel.color.thresholds.push({ color: "#cb4b16", value: 1, tooltip: "orange" });
_this.panel.color.thresholds.push({ color: "#dc322f", value: 2, tooltip: "red" });
_this.panel.color.thresholds.push({ color: "#d33682", value: 3, tooltip: "magenta" });
_this.panel.color.thresholds.push({ color: "#6c71c4", value: 4, tooltip: "violet" });
_this.panel.color.thresholds.push({ color: "#268bd2", value: 5, tooltip: "blue" });
_this.panel.color.thresholds.push({ color: "#2aa198", value: 6, tooltip: "cyan" });
_this.panel.color.thresholds.push({ color: "#859900", value: 7, tooltip: "green" });
_this.render();
};
_this.link = function (scope, elem, attrs, ctrl) {
rendering(scope, elem, attrs, ctrl);
};
@@ -242,51 +327,54 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
maxValue: 0,
minValue: 0,
multipleValues: false,
noColorDefined: false
noColorDefined: false,
targets: [], // array of available unique targets
targetIndex: {} // indices in data array for each of available unique targets
};
if (!data || data.length == 0) {
return cardsData;
}
// collect uniq targets and their indexes in data array
cardsData.targetIndex = {};
for (var i = 0; i < data.length; i++) {
var ts = data[i];
var target = ts.target;
if (cardsData.targetIndex[target] == undefined) {
cardsData.targetIndex[target] = [];
}
cardsData.targetIndex[target].push(i);
}
// Collect uniq timestamps from data and spread over targets and timestamps
// collect uniq targets and their indices
_.map(data, function (d, i) {
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i);
});
// TODO add some logic for targets heirarchy
cardsData.targets = _.keys(cardsData.targetIndex);
cardsData.targets.sort();
cardsData.yBucketSize = cardsData.targets.length;
cardsData.xBucketSize = _.min(_.map(data, function (d) {
// Maximum number of buckets over x axis
cardsData.xBucketSize = _.max(_.map(data, function (d) {
return d.datapoints.length;
}));
// Collect all values for each bucket from datapoints with similar target.
for (var _i = 0; _i < cardsData.targets.length; _i++) {
var _target = cardsData.targets[_i];
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
for (var i = 0; i < cardsData.targets.length; i++) {
var target = cardsData.targets[i];
for (var j = 0; j < cardsData.xBucketSize; j++) {
var card = {
id: _i * cardsData.xBucketSize + j,
id: i * cardsData.xBucketSize + j,
values: [],
multipleValues: false,
noColorDefined: false
noColorDefined: false,
y: target,
x: -1
};
// collect values from all timeseries with target
for (var si = 0; si < cardsData.targetIndex[_target].length; si++) {
var s = data[cardsData.targetIndex[_target][si]];
for (var si = 0; si < cardsData.targetIndex[target].length; si++) {
var s = data[cardsData.targetIndex[target][si]];
if (s.datapoints.length <= j) {
continue;
}
var datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
card.y = s.target;
}
card.values.push(datapoint[VALUE_INDEX]);
}
@@ -304,7 +392,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
if (cardsData.minValue > card.minValue) cardsData.minValue = card.minValue;
cardsData.cards.push(card);
if (card.x != -1) {
cardsData.cards.push(card);
}
}
}
@@ -338,12 +428,16 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
}
};
_this.annotations = [];
_this.annotationsSrv = annotationsSrv;
_this.events.on('data-received', _this.onDataReceived);
_this.events.on('data-snapshot-load', _this.onDataReceived);
_this.events.on('data-error', _this.onDataError);
_this.events.on('init-edit-mode', _this.onInitEditMode);
_this.events.on('render', _this.onRender);
_this.events.on('refresh', _this.postRefresh);
// custom event from rendering.js
_this.events.on('render-complete', _this.onRenderComplete);
return _this;
}
@@ -351,6 +445,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
// override calculateInterval for discrete color mode
/* https://ethanschoonover.com/solarized/ */
// group values into buckets by target
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -88,7 +88,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
}, {
key: 'add',
value: function add() {
this.tooltip = d3.select("body").append("div").attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip");
this.tooltip = d3.select("body").append("div").attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
}
}, {
key: 'destroy',
@@ -128,7 +128,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
var time = this.dashboard.formatDate(+x, tooltipTimeFormat);
var tooltipHtml = '<div class="graph-tooltip-time">' + time + '</div>\n <div class="status-heatmap-histogram"></div>';
var tooltipHtml = '<div class="graph-tooltip-time">' + time + '</div>\n <div class="statusmap-histogram"></div>';
if (this.panel.color.mode === 'discrete') {
var statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
@@ -139,7 +139,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
statusesHtml = "statuses:";
}
tooltipHtml += '\n <div>\n name: <b>' + y + '</b> <br>\n ' + statusesHtml + '\n <ul>\n ' + _.join(_.map(statuses, function (v) {
return '<li style="background-color: ' + v.color + '; padding: 1px; font-weight: bold; text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF">' + v.tooltip + '</li>';
return '<li style="background-color: ' + v.color + '" class="discrete-item">' + v.tooltip + '</li>';
}), "") + '\n </ul>\n </div>';
} else {
if (values.length === 1) {
+1
View File
File diff suppressed because one or more lines are too long
+398
View File
@@ -0,0 +1,398 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 8,
"iteration": 1545919919137,
"links": [],
"panels": [
{
"cards": {
"cardHSpacing": 2,
"cardMinWidth": 5,
"cardRound": null,
"cardVSpacing": 2
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"defaultColor": "#757575",
"exponent": 0.5,
"max": 5,
"min": 1,
"mode": "discrete",
"thresholds": [
{
"color": "#cca300",
"tooltip": "Pending",
"value": "1"
},
{
"color": "#9ac48a",
"tooltip": "Running",
"value": "2"
},
{
"color": "#bf1b00",
"tooltip": "Failed",
"value": "3"
},
{
"color": "#3f6833",
"tooltip": "Succeeded",
"value": "4"
},
{
"color": "#f9e2d2",
"tooltip": "Unknown",
"value": "5"
}
]
},
"data": {
"decimals": null,
"unitFormat": "short"
},
"datasource": "prometheus",
"gridPos": {
"h": 16,
"w": 24,
"x": 0,
"y": 0
},
"highlightCards": true,
"id": 2,
"interval": "",
"legend": {
"show": true
},
"links": [],
"nullPointMode": "as empty",
"repeat": null,
"repeatDirection": null,
"targets": [
{
"expr": "kube_pod_status_phase{namespace=\"$namespace\", phase=\"Pending\"} == 1",
"format": "time_series",
"hide": false,
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{ pod }}",
"refId": "A"
},
{
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Running\"} == 1) * 2",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }}",
"refId": "B"
},
{
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Failed\"} == 1) * 3",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }}",
"refId": "C"
},
{
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Succeeded\"} == 1) * 4",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }}",
"refId": "D"
},
{
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Unknown\"} == 1) * 5",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }}",
"refId": "E"
}
],
"title": "Pod Status",
"tooltip": {
"show": true
},
"transparent": true,
"type": "flant-statusmap-panel",
"useMax": true,
"xAxis": {
"labelFormat": "%a %m/%d",
"minBucketWidthToShowWeekends": 4,
"show": true,
"showCrosshair": true,
"showWeekends": true
},
"yAxis": {
"show": true,
"showCrosshair": false
},
"yAxisSort": "a → z"
},
{
"cards": {
"cardHSpacing": 2,
"cardMinWidth": 5,
"cardRound": null,
"cardVSpacing": 2
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateOranges",
"defaultColor": "#757575",
"exponent": 0.5,
"max": 5,
"min": 1,
"mode": "discrete",
"thresholds": [
{
"color": "#cca300",
"tooltip": "ContainerCreating",
"value": "1"
},
{
"color": "#890f02",
"tooltip": "CrashLoopBackOff",
"value": "2"
},
{
"color": "#bf1b00",
"tooltip": "ErrImagePull",
"value": "3"
},
{
"color": "#58140c",
"tooltip": "ImagePullBackOff",
"value": "4"
},
{
"color": "#7eb26d",
"tooltip": "Running",
"value": "5"
},
{
"color": "#052b51",
"tooltip": "Completed",
"value": "6"
},
{
"color": "#c15c17",
"tooltip": "ContainerCannotRun",
"value": "7"
},
{
"color": "#f9934e",
"tooltip": "Error",
"value": "8"
},
{
"color": "#eab839",
"tooltip": "OOMKilled",
"value": "9"
}
]
},
"data": {
"decimals": null,
"unitFormat": "short"
},
"datasource": "prometheus",
"gridPos": {
"h": 16,
"w": 24,
"x": 0,
"y": 16
},
"highlightCards": true,
"id": 3,
"interval": "",
"legend": {
"show": true
},
"links": [],
"nullPointMode": "as empty",
"repeatDirection": null,
"targets": [
{
"expr": "kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ContainerCreating\"} == 1",
"format": "time_series",
"hide": false,
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "A"
},
{
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"CrashLoopBackOff\"} == 1) * 2",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "B"
},
{
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ErrImagePull\"} == 1) * 3",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "C"
},
{
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ImagePullBackOff\"} == 1) * 4",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "D"
},
{
"expr": "(kube_pod_container_status_running{namespace=\"$namespace\"} == 1) * 5",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "E"
},
{
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"Completed\"} == 1) * 6",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "F"
},
{
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"ContainerCannotRun\"} == 1) * 7",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "G"
},
{
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"Error\"} == 1) * 8",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "H"
},
{
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"OOMKilled\"} == 1) * 9",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"legendFormat": "{{ pod }} - {{ container }}",
"refId": "I"
}
],
"title": "Container Status",
"tooltip": {
"show": true
},
"transparent": true,
"type": "flant-statusmap-panel",
"useMax": true,
"xAxis": {
"labelFormat": "%a %m/%d",
"minBucketWidthToShowWeekends": 4,
"show": true,
"showCrosshair": true,
"showWeekends": true
},
"yAxis": {
"show": true,
"showCrosshair": false
},
"yAxisSort": "a → z"
}
],
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {
"tags": [],
"text": "openshift-monitoring",
"value": "openshift-monitoring"
},
"datasource": "prometheus",
"definition": "label_values(kube_pod_info, namespace)",
"hide": 0,
"includeAll": false,
"label": "Namespace",
"multi": false,
"name": "namespace",
"options": [],
"query": "label_values(kube_pod_info, namespace)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 2,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Pod status",
"uid": "x3PAP_ymk",
"version": 10
}
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "status-hetamap",
"version": "0.0.1",
"description": "Status Heatmap panel plugin for grafana",
"name": "flant-statusmap-panel",
"version": "0.1.0",
"description": "Grafana panel plugin to visualize status of multiple objects over time",
"main": "README.md",
"scripts": {
"build": "grunt"
@@ -10,7 +10,7 @@
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/flant/grafana-status-heatmap.git"
"url": "https://github.com/flant/grafana-statusmap.git"
},
"devDependencies": {
"babel": "^6.23.0",
@@ -27,6 +27,7 @@
"grunt-contrib-uglify": "^3.0.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-execute": "^0.2.2",
"grunt-notify": "^0.4.5",
"grunt-systemjs-builder": "^1.0.0",
"load-grunt-tasks": "^3.5.2"
},
+134
View File
@@ -0,0 +1,134 @@
import d3 from 'd3';
import $ from 'jquery';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
let TOOLTIP_PADDING_X = 30;
let TOOLTIP_PADDING_Y = 10;
export class AnnotationTooltip {
constructor(elem, scope) {
this.scope = scope;
this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel;
this.mouseOverAnnotationTick = false;
elem.on("mouseover", this.onMouseOver.bind(this));
elem.on("mouseleave", this.onMouseLeave.bind(this));
}
onMouseOver(e) {
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; }
if (!this.tooltip) {
this.add();
this.move(e);
}
}
onMouseLeave() {
this.destroy();
}
onMouseMove(e) {
if (!this.panel.tooltip.show) { return; }
this.move(e);
}
add() {
this.tooltipBase = d3.select("body")
.append("div")
.attr("class", "statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open")
.style("position", "absolute")
this.tooltip = this.tooltipBase
.append("div")
.attr("class", "drop-content")
.append("div")
.append("annotation-tooltip")
.append("div")
.attr("class", "graph-annotation");
}
destroy() {
if (this.tooltip) {
this.tooltip.remove();
}
this.tooltip = null;
if (this.tooltipBase) {
this.tooltipBase.remove();
}
this.tooltipBase = null;
}
show(pos) {
if (!this.panel.tooltip.show || !this.tooltip) { return; }
// shared tooltip mode
//if (pos.panelRelY) {
// return;
//}
let annoId = d3.select(pos.target).attr('annoId');
if (!annoId) {
this.destroy();
return;
}
let anno = this.panelCtrl.annotations[annoId];
if (!anno) {
this.destroy();
return;
}
let annoTitle = "";
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
let annoTime = this.dashboard.formatDate(anno.time, tooltipTimeFormat);
let annoText = anno.text;
let annoTags = [];
if (anno.tags) {
annoTags = _.map(anno.tags, t => ({"text": t, "backColor": "rgb(63, 43, 91)", "borderColor":"rgb(101, 81, 129)"}))
}
let tooltipHtml = `<div class="graph-annotation__header">
<span class="graph-annotation__title">${annoTitle}</span>
<span class="graph-annotation__time">${annoTime}</span></div>
<div class="graph-annotation__body">
<div>${annoText}</div>
${_.join(_.map(annoTags, t => `<span class="label label-tag small" style="background-color: ${t.backColor}; border-color: ${t.borderColor}">${t.text}</span>`), "")}
</div>
<div class="statusmap-histogram"></div>`;
this.tooltip.html(tooltipHtml);
this.move(pos);
}
move(pos) {
if (!this.tooltipBase) { return; }
let elem = $(this.tooltipBase.node())[0];
let tooltipWidth = elem.clientWidth;
let tooltipHeight = elem.clientHeight;
let left = pos.pageX - tooltipWidth/2;
let top = pos.pageY + TOOLTIP_PADDING_Y;
if (pos.pageX + tooltipWidth/2 + 10 > window.innerWidth) {
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
}
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
}
return this.tooltipBase
.style("left", left + "px")
.style("top", top + "px");
}
}
+38 -41
View File
@@ -8,10 +8,10 @@ import {tickStep} from 'app/core/utils/ticks';
let mod = angular.module('grafana.directives');
const MIN_LEGEND_STEPS = 10;
const LEGEND_STEP_WIDTH = 2;
/**
* Color legend for heatmap editor.
* Bigger color legend for opacity and spectrum modes editor.
*/
mod.directive('optionsColorLegend', function() {
return {
@@ -45,7 +45,7 @@ mod.directive('optionsColorLegend', function() {
});
/**
* Heatmap legend with scale values.
* Graph legend with values.
*/
mod.directive('statusHeatmapLegend', function() {
return {
@@ -62,6 +62,9 @@ mod.directive('statusHeatmapLegend', function() {
function render() {
clearLegend(elem);
if (!ctrl.panel.legend.show) {
return
}
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
let rangeFrom = ctrl.cardsData.minValue;
let rangeTo = ctrl.cardsData.maxValue;
@@ -89,18 +92,12 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
let legendHeight = legendElem.attr("height");
let rangeStep = 1;
if (rangeTo - rangeFrom > legendWidth) {
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
}
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
rangeStep = rangeTo / MIN_LEGEND_STEPS;
}
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
// width in pixels in legend space of unit segment in range space
// rangeStep * witdhFactor == width in pixels of one rangeStep
let widthFactor = legendWidth / (rangeTo - rangeFrom);
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
@@ -108,9 +105,12 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
legend.selectAll(".status-heatmap-color-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d * widthFactor)
// translate from range space into pixels
// and shift all rectangles to the right by 10
.attr("x", d => d * widthFactor+10)
.attr("y", 0)
.attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
// rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH+1)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => colorScale(d));
@@ -123,25 +123,25 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
let legendHeight = legendElem.attr("height");
let rangeStep = 10;
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
// width in pixels in legend space of unit segment in range space
// rangeStep * witdhFactor == width in pixels of one rangeStep
let widthFactor = legendWidth / (rangeTo - rangeFrom);
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
rangeStep = rangeTo / MIN_LEGEND_STEPS;
}
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
let opacityScale = getOpacityScale(options, maxValue, minValue);
legend.selectAll(".status-heatmap-opacity-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d * widthFactor)
// translate from range space into pixels
// and shift all rectangles to the right by 10
.attr("x", d => d * widthFactor+10)
.attr("y", 0)
.attr("width", rangeStep * widthFactor)
// rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH+1)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", options.cardColor)
@@ -168,14 +168,14 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
legend.selectAll(".hidden-texts")
.data(tooltips)
.enter().append("text")
.attr("class", "axis tick")
.attr("class", "axis tick hidden-texts")
.attr("font-family", "sans-serif")
.text(d => d)
.each(function(d,i) {
let thisWidth = this.getBBox().width;
textWidth.push(thisWidth);
this.remove(); // remove them just after displaying them
});
legend.selectAll(".hidden-texts").remove();
let legendWidth = Math.floor(_.min([
graphWidth - 30,
@@ -289,23 +289,21 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
function drawSimpleColorLegend(elem, colorScale) {
let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth());
let legendHeight = legendElem.attr("height");
if (legendWidth) {
let valuesNumber = Math.floor(legendWidth / 2);
let rangeStep = Math.floor(legendWidth / valuesNumber);
let valuesRange = d3.range(0, legendWidth, rangeStep);
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
let legend = d3.select(legendElem.get(0));
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
legendRects.enter().append("rect")
legend.selectAll(".status-heatmap-color-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
.attr("width", LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => colorScale(d));
@@ -314,10 +312,9 @@ function drawSimpleColorLegend(elem, colorScale) {
function drawSimpleOpacityLegend(elem, options) {
let legendElem = $(elem).find('svg');
let graphElem = $(elem);
let legend = d3.select(legendElem.get(0));
clearLegend(elem);
let legend = d3.select(legendElem.get(0));
let legendWidth = Math.floor(legendElem.outerWidth());
let legendHeight = legendElem.attr("height");
@@ -333,14 +330,14 @@ function drawSimpleOpacityLegend(elem, options) {
.range([0, 1]);
}
let rangeStep = 10;
let valuesRange = d3.range(0, legendWidth, rangeStep);
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
legendRects.enter().append("rect")
legend.selectAll(".status-heatmap-opacity-legend-rect")
.data(valuesRange)
.enter().append("rect")
.attr("x", d => d)
.attr("y", 0)
.attr("width", rangeStep)
.attr("width", LEGEND_STEP_WIDTH+1)
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", options.cardColor)
+25 -1
View File
@@ -61,6 +61,30 @@ export class ColorModeDiscrete {
getBucketColor(values) {
let thresholds = this.panel.color.thresholds;
if (!values || values.length == 0) {
// treat as null value
return this.getMatchedThreshold(null).color;
}
if (values.length == 1) {
let threshold = this.getMatchedThreshold(values[0]);
if (!threshold || !threshold.color || threshold.color == "") {
return 'rgba(0,0,0,1)';
} else {
return threshold.color;
}
}
let isAllValuesNulls = true;
for (let j = 0; j < values.length; j++) {
if (values[j] != null) {
isAllValuesNulls = false;
}
}
if (isAllValuesNulls) {
return this.getMatchedThreshold(null).color;
}
for (let i = 0; i < thresholds.length; i++) {
for (let j = 0; j < values.length; j++) {
if (values[j] == thresholds[i].value) {
@@ -93,7 +117,7 @@ export class ColorModeDiscrete {
getMatchedThreshold(value) {
if (value == null) {
if (this.panel.color.nullPointMode == 'as empty') {
if (this.panel.nullPointMode == 'as empty') {
// FIXME: make this explicit for user
// Right now this color never used because null as empty handles in getCardOpacity method.
return {
@@ -1,12 +1,15 @@
$font-size-sm: 12px !default;
.status-heatmap-canvas-wrapper {
// position: relative;
cursor: crosshair;
// allow tooltips on hover of warning text
.datapoints-warning {
pointer-events: none;
}
.datapoints-warning span {
background-color: white;
background-color: $warning-bg;
color: $text-color;
padding: 1px;
}
}
@@ -16,14 +19,14 @@ $font-size-sm: 12px !default;
.axis .tick {
text {
fill: #52545c;
color: #52545c;
font-size: 11px;
fill: $text-color;
color: $text-color;
font-size: $font-size-tick;
}
line {
opacity: 0.4;
stroke: #767980;
stroke: $text-color-weak;
}
}
@@ -37,20 +40,27 @@ $font-size-sm: 12px !default;
}
}
.status-heatmap-tooltip {
.statusmap-tooltip {
white-space: nowrap;
font-size: $font-size-sm;
background-color: #dde4ed;
color: #52545c;
background-color: $graph-tooltip-bg;
color: $text-color;
.discrete-item {
color: #52545c;
padding: 1px;
font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF;
}
}
.status-heatmap-histogram rect {
fill: #767980;
.statusmap-histogram rect {
fill: $text-color-weak;
}
.status-heatmap-crosshair {
line {
stroke: darken(red,15%);
stroke: darken($red,15%);
stroke-width: 1;
}
}
@@ -61,14 +71,11 @@ $font-size-sm: 12px !default;
stroke: rgba(102, 102, 102, 0.8);
}
.status-heatmap-legend-wrapper {
// @include clearfix();
margin: 0 10px;
//padding-top: 10px;
svg {
//width: 100%;
//max-width: 1300px;
height: 24px;
float: left;
white-space: nowrap;
@@ -85,19 +92,19 @@ $font-size-sm: 12px !default;
.axis .tick {
text {
fill: #52545c;
color: #52545c;
font-size: 11px;
fill: $text-color;
color: $text-color;
font-size: $font-size-tick;
}
line {
opacity: 0.4;
stroke: #767980;
stroke: $text-color-weak;
}
.domain {
opacity: 0.4;
stroke: #767980;
stroke: $text-color-weak;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
// Dark variables
$text-color: #d8d9da; // $gray-4
$text-color-weak: #8e8e8e; // $gray-2
$graph-tooltip-bg: #141414; // $dark-1
$warning-bg: #333333; // $dark-4
$theme-name: dark;
$white: #fff;
$red: #d44a3a;
+10
View File
@@ -0,0 +1,10 @@
// Light variables
$text-color: #52545c; // $gray-1
$text-color-weak: #767980; // $gray-2
$graph-tooltip-bg: #dde4ed; // $gray-5
$warning-bg: #e9edf2; // $gray-6
$theme-name: light;
$white: #fff;
$red: #d44939;
+3
View File
@@ -0,0 +1,3 @@
$font-size-sm: 12px !default;
$font-size-tick: 11px;
+3
View File
@@ -0,0 +1,3 @@
@import "variables";
@import "variables.dark";
@import "statusmap"
+3
View File
@@ -0,0 +1,3 @@
@import "variables";
@import "variables.light";
@import "statusmap"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
</div>
+6 -1
View File
@@ -1,6 +1,11 @@
import './color_legend';
import {loadPluginCss} from 'app/plugins/sdk';
import { StatusHeatmapCtrl } from './status_heatmap_ctrl';
loadPluginCss({
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
});
export {
StatusHeatmapCtrl as PanelCtrl
};
+78 -35
View File
@@ -55,40 +55,70 @@
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
</div>
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
<label class="gf-form-label width-2">{{ $index }}</label>
<label class="gf-form-label width-9" ng-if="ctrl.panel.useMax">If bucket has value</label>
<label class="gf-form-label width-9" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
<label class="gf-form-label">
fill
</label>
<label class="gf-form-label">
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
</label>
<label class="gf-form-label">set tooltip</label>
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.onEditorRemoveThreshold($index)">
<i class="fa fa-trash"/>
</a>
</label>
<div class="gf-form" ng-show="ctrl.panel.color.thresholds.length == 0">
<label class="gf-form-label width-2">0</label>
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</label>
</div>
<div class="gf-form">
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
Add trafic lights
</a>
</label>
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
Add new status
</a>
</label>
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
<div class="gf-form">
<label class="gf-form-label width-2">{{ $index }}</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
<label class="gf-form-label query-keyword">
fill
</label>
<label class="gf-form-label">
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
</label>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">set tooltip</label>
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
<i class="fa fa-trash"/>
</a>
</label>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form"></div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()">
<i class="fa fa-plus"></i> Add new status
</button>
</div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()">
<i class="fa fa-minus"></i> Remove all
</button>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.panel.color.thresholds.length == 0">
<div class="gf-form"></div>
<div class="gf-form">
<label class="gf-form-label">Presets</label>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()">
Red-Yellow-Green
</button>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddSolarized()">
Solarized
</button>
</div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
@@ -118,7 +148,16 @@
label="Show tooltip"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Y axis sort</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-8"
ng-model="ctrl.panel.yAxisSort"
ng-options="f for f in ['metrics', 'a → z', 'z → a']"
ng-change="ctrl.render()"></select>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Bucket</h5>
@@ -142,12 +181,16 @@
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
<label class="gf-form-label width-9">V spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards vertical spacing in pixels'" ng-model="ctrl.panel.cards.cardVSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">H spacing</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Rounding</label>
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Angles rounding in percent'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
</div>
</div>
+4 -3
View File
@@ -20,12 +20,13 @@
],
"screenshots": [
{"name": "Panel", "path": "img/flant-statusmap-panel.png"},
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
{"name": "Panel with dark theme", "path": "img/flant-statusmap-panel-dark.png"},
{"name": "Color mapping", "path": "img/color-mapping.png"},
{"name": "Queries example", "path": "img/queries-example.png"}
],
"version": "0.0.1",
"updated": "2018-09-17"
"version": "0.1.1",
"updated": "2019-03-22"
},
"dependencies": {
+98 -16
View File
@@ -7,9 +7,11 @@ import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks
import d3 from 'd3';
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {StatusHeatmapTooltip} from './tooltip';
import {AnnotationTooltip} from './annotations';
let MIN_CARD_SIZE = 5,
CARD_SPACING = 2,
CARD_H_SPACING = 2,
CARD_V_SPACING = 2,
CARD_ROUND = 0,
DATA_RANGE_WIDING_FACTOR = 1.2,
DEFAULT_X_TICK_SIZE_PX = 100,
@@ -24,13 +26,14 @@ export default function link(scope, elem, attrs, ctrl) {
// $heatmap is JQuery object, but heatmap is D3
let $heatmap = elem.find('.status-heatmap-panel');
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
let annotationTooltip = new AnnotationTooltip($heatmap, scope);
let width, height,
yScale, xScale,
chartWidth, chartHeight,
chartTop, chartBottom,
yAxisWidth, xAxisHeight,
cardSpacing, cardRound,
cardVSpacing, cardHSpacing, cardRound,
cardWidth, cardHeight,
colorScale, opacityScale,
mouseUpHandler,
@@ -76,7 +79,7 @@ export default function link(scope, elem, attrs, ctrl) {
return text.getBBox().width;
}));
return max_text_width;
return Math.ceil(max_text_width);
}
function getXAxisHeight(elem) {
@@ -129,9 +132,10 @@ export default function link(scope, elem, attrs, ctrl) {
function getYScale(ticks) {
let range = [];
let step = chartHeight / ticks.length;
range.push(chartHeight);
// svg has y=0 on the top, so top card should have a minimal value in range
range.push(step);
for (let i = 1; i < ticks.length; i++) {
range.push(chartHeight - step * i);
range.push(step * (i+1));
}
return d3.scaleOrdinal()
.domain(ticks)
@@ -142,9 +146,10 @@ export default function link(scope, elem, attrs, ctrl) {
function getYAxisScale(ticks) {
let range = [];
let step = chartHeight / ticks.length;
range.push(chartHeight - yOffset);
// svg has y=0 on the top, so top tick should have a minimal value in range
range.push(yOffset);
for (let i = 1; i < ticks.length; i++) {
range.push(chartHeight - step * i - yOffset);
range.push(step * i + yOffset);
}
return d3.scaleOrdinal()
.domain(ticks)
@@ -159,6 +164,12 @@ export default function link(scope, elem, attrs, ctrl) {
ticks = [''];
}
if (panel.yAxisSort == 'a → z') {
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
} else if (panel.yAxisSort == 'z → a') {
ticks.sort((b, a) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
}
let yAxisScale = getYAxisScale(ticks);
scope.yScale = yScale = getYScale(ticks);
@@ -231,12 +242,13 @@ export default function link(scope, elem, attrs, ctrl) {
chartTop = margin.top;
chartBottom = chartTop + chartHeight;
cardSpacing = panel.cards.cardSpacing !== null ? panel.cards.cardSpacing : CARD_SPACING;
cardHSpacing = panel.cards.cardHSpacing !== null ? panel.cards.cardHSpacing : CARD_H_SPACING;
cardVSpacing = panel.cards.cardVSpacing !== null ? panel.cards.cardVSpacing : CARD_V_SPACING;
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
// calculate yOffset for YAxis
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
yOffset = cardHeight / 2;
addYAxis();
@@ -244,9 +256,10 @@ export default function link(scope, elem, attrs, ctrl) {
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
chartWidth = width - yAxisWidth - margin.right;
// TODO allow per-y cardWidth!
// we need to fill chartWidth with xBucketSize cards.
xGridSize = chartWidth / (cardsData.xBucketSize+1);
cardWidth = xGridSize - cardSpacing;
cardWidth = xGridSize - cardHSpacing;
addXAxis();
xAxisHeight = getXAxisHeight(heatmap);
@@ -299,6 +312,8 @@ export default function link(scope, elem, attrs, ctrl) {
resetCardHighLight(event);
});
_renderAnnotations();
ctrl.events.emit('render-complete', {
"chartWidth": chartWidth
});
@@ -356,7 +371,7 @@ export default function link(scope, elem, attrs, ctrl) {
let cx = xScale(d.x);
if (cx - cardWidth/2 < 0) {
x = yAxisWidth + cardSpacing/2;
x = yAxisWidth + cardHSpacing/2;
} else {
x = yAxisWidth + cx - cardWidth/2;
}
@@ -372,11 +387,11 @@ export default function link(scope, elem, attrs, ctrl) {
if (cx < cardWidth/2) {
// Center should not exceed half of card.
// Cut card to the left to prevent overlay of y axis.
let cutted_width = (cx - cardSpacing/2) + cardWidth/2;
let cutted_width = (cx - cardHSpacing/2) + cardWidth/2;
w = cutted_width > 0 ? cutted_width : 0;
} else if (chartWidth - cx < cardWidth/2) {
// Cut card to the right to prevent overlay of right graph edge.
w = cardWidth/2 + (chartWidth - cx - cardSpacing/2);
w = cardWidth/2 + (chartWidth - cx - cardHSpacing/2);
} else {
w = cardWidth;
}
@@ -384,21 +399,25 @@ export default function link(scope, elem, attrs, ctrl) {
// Card width should be MIN_CARD_SIZE at least
w = Math.max(w, MIN_CARD_SIZE);
if (cardHSpacing == 0) {
w = w+1;
}
return w;
}
function getCardY(d) {
return yScale(d.y) + chartTop - cardHeight - cardSpacing/2;
return yScale(d.y) + chartTop - cardHeight - cardVSpacing/2;
}
function getCardHeight(d) {
let ys = yScale(d.y);
let y = ys + chartTop - cardHeight - cardSpacing/2;
let y = ys + chartTop - cardHeight - cardVSpacing/2;
let h = cardHeight;
// Cut card height to prevent overlay
if (y < chartTop) {
h = ys - cardSpacing/2;
h = ys - cardVSpacing/2;
} else if (ys > chartBottom) {
h = chartBottom - y;
} else if (y + cardHeight > chartBottom) {
@@ -410,6 +429,10 @@ export default function link(scope, elem, attrs, ctrl) {
// Card height should be MIN_CARD_SIZE at least
h = Math.max(h, MIN_CARD_SIZE);
if (cardVSpacing == 0) {
h = h+1
}
return h;
}
@@ -487,6 +510,7 @@ export default function link(scope, elem, attrs, ctrl) {
function onMouseLeave() {
appEvents.emit('graph-hover-clear');
clearCrosshair();
//annotationTooltip.destroy();
}
function onMouseMove(event) {
@@ -496,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
// Clear crosshair and tooltip
clearCrosshair();
tooltip.destroy();
annotationTooltip.destroy();
selection.x2 = limitSelection(event.offsetX);
drawSelection(selection.x1, selection.x2);
@@ -503,6 +528,7 @@ export default function link(scope, elem, attrs, ctrl) {
emitGraphHoverEvet(event);
drawCrosshair(event.offsetX);
tooltip.show(event); //, data);
annotationTooltip.show(event);
}
}
@@ -618,6 +644,62 @@ export default function link(scope, elem, attrs, ctrl) {
$heatmap.on("mousedown", onMouseDown);
$heatmap.on("mousemove", onMouseMove);
$heatmap.on("mouseleave", onMouseLeave);
function _renderAnnotations() {
if (!ctrl.annotations || ctrl.annotations.length == 0) {
return;
}
if (!heatmap) {
return;
}
let annoData = _.map(ctrl.annotations, (d,i) => ({"x": Math.floor(yAxisWidth + xScale(d.time)), "id":i, "anno": d.source}));
let anno = heatmap
.append("g")
.attr("class", "statusmap-annotations")
.attr("transform", "translate(0.5,0)")
.selectAll(".statusmap-annotations")
.data(annoData)
.enter().append("g")
;
anno.append("line")
//.attr("class", "statusmap-annotation-tick")
.attr("x1", d => d.x)
.attr("y1", chartTop)
.attr("x2", d => d.x)
.attr("y2", chartBottom)
.style("stroke", d => d.anno.iconColor)
.style("stroke-width", 1)
.style("stroke-dasharray", "3,3")
;
anno.append("polygon")
.attr("points", d => [[d.x, chartBottom+1], [d.x-5, chartBottom+6], [d.x+5, chartBottom+6]].join(" "))
.style("stroke-width", 0)
.style("fill", d => d.anno.iconColor)
;
// Polygons didn't fire mouseevents
anno.append("rect")
.attr("x", d => d.x-5)
.attr("width", 10)
.attr("y", chartBottom+1)
.attr("height", 5)
.attr("class", "statusmap-annotation-tick")
.attr("annoId", d => d.id)
.style("opacity", 0)
;
let $ticks = $heatmap.find(".statusmap-annotation-tick");
$ticks.on("mouseenter", (event) => {
annotationTooltip.mouseOverAnnotationTick = true;
})
.on("mouseleave", (event) => {
annotationTooltip.mouseOverAnnotationTick = false;
});
}
}
function grafanaTimeFormat(ticks, min, max) {
+93 -19
View File
@@ -3,13 +3,13 @@ import _ from 'lodash';
import { contextSrv } from 'app/core/core';
import kbn from 'app/core/utils/kbn';
import './color_legend';
import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates';
// import fragments, { fragmentsMap } from './fragments';
// import { labelFormats } from './xAxisLabelFormats';
import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete";
import './css/status-heatmap.css!';
const CANVAS = 'CANVAS';
const SVG = 'SVG';
@@ -31,7 +31,8 @@ const panelDefaults = {
},
cards: {
cardMinWidth: 5,
cardSpacing: 2,
cardVSpacing: 2,
cardHSpacing: 2,
cardRound: null
},
xAxis: {
@@ -57,6 +58,7 @@ const panelDefaults = {
},
// how null points should be handled
nullPointMode: 'as empty',
yAxisSort: 'metrics',
highlightCards: true,
useMax: true
};
@@ -89,7 +91,7 @@ const colorSchemes = [
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
{ name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }
];
let colorModes = ['opacity', 'spectrum', 'discrete'];
@@ -98,7 +100,8 @@ let opacityScales = ['linear', 'sqrt'];
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
constructor($scope, $injector, $rootScope, timeSrv) {
/** @ngInject */
constructor($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
super($scope, $injector);
_.defaultsDeep(this.panel, panelDefaults);
@@ -127,17 +130,22 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
}
};
this.annotations = [];
this.annotationsSrv = annotationsSrv;
this.events.on('data-received', this.onDataReceived);
this.events.on('data-snapshot-load', this.onDataReceived);
this.events.on('data-error', this.onDataError);
this.events.on('init-edit-mode', this.onInitEditMode);
this.events.on('render', this.onRender);
this.events.on('refresh', this.postRefresh);
// custom event from rendering.js
this.events.on('render-complete', this.onRenderComplete);
}
onRenderComplete = (data) => {
this.graph.chartWidth = data.chartWidth;
this.renderingCompleted();
};
// override calculateInterval for discrete color mode
@@ -150,7 +158,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
]);
let minCardWidth = this.panel.cards.cardMinWidth;
let minSpacing = this.panel.cards.cardSpacing;
let minSpacing = this.panel.cards.cardHSpacing;
let maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
let intervalMs;
@@ -185,11 +193,53 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.interval = kbn.secondsToHms(intervalMs / 1000);
};
issueQueries = (datasource) => {
this.annotationsPromise = this.annotationsSrv.getAnnotations({
dashboard: this.dashboard,
panel: this.panel,
range: this.range,
});
/* Wait for annotationSrv requests to get datasources to
* resolve before issuing queries. This allows the annotations
* service to fire annotations queries before graph queries
* (but not wait for completion). This resolves
* issue 11806.
*/
// 5.x before 5.4 doesn't have dataPromises
if ("undefined" !== typeof(this.annotationsSrv.datasourcePromises)) {
return this.annotationsSrv.datasourcePromises.then(r => {
return super.issueQueries(datasource);
});
} else {
return super.issueQueries(datasource);
}
}
onDataReceived = (dataList) => {
this.data = dataList;
this.cardsData = this.convertToCards(this.data);
this.render();
this.annotationsPromise.then(
result => {
this.loading = false;
//this.alertState = result.alertState;
if (result.annotations && result.annotations.length > 0) {
this.annotations = result.annotations;
} else {
this.annotations = null;
}
this.render();
},
() => {
this.loading = false;
this.annotations = null;
this.render();
}
);
//this.render();
};
onInitEditMode = () => {
@@ -223,6 +273,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
onDataError = () => {
this.data = [];
this.annotations = [];
this.render();
};
@@ -240,12 +291,30 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.render();
};
onEditorRemoveThresholds = () => {
this.panel.color.thresholds = [];
this.render();
};
onEditorAddThreeLights = () => {
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
this.render();
};
/* https://ethanschoonover.com/solarized/ */
onEditorAddSolarized = () => {
this.panel.color.thresholds.push({color: "#b58900", value: 0, tooltip: "yellow" });
this.panel.color.thresholds.push({color: "#cb4b16", value: 1, tooltip: "orange" });
this.panel.color.thresholds.push({color: "#dc322f", value: 2, tooltip: "red" });
this.panel.color.thresholds.push({color: "#d33682", value: 3, tooltip: "magenta" });
this.panel.color.thresholds.push({color: "#6c71c4", value: 4, tooltip: "violet" });
this.panel.color.thresholds.push({color: "#268bd2", value: 5, tooltip: "blue" });
this.panel.color.thresholds.push({color: "#2aa198", value: 6, tooltip: "cyan" });
this.panel.color.thresholds.push({color: "#859900", value: 7, tooltip: "green" });
this.render();
}
link = (scope, elem, attrs, ctrl) => {
rendering(scope, elem, attrs, ctrl);
@@ -261,28 +330,27 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
minValue: 0,
multipleValues: false,
noColorDefined: false,
targets: [], // array of available unique targets
targetIndex: {} // indices in data array for each of available unique targets
};
if (!data || data.length == 0) { return cardsData;}
// collect uniq targets and their indexes in data array
cardsData.targetIndex = {};
for (let i = 0; i < data.length; i++) {
let ts = data[i];
let target = ts.target;
if (cardsData.targetIndex[target] == undefined) {
cardsData.targetIndex[target] = []
}
cardsData.targetIndex[target].push(i);
}
// Collect uniq timestamps from data and spread over targets and timestamps
// collect uniq targets and their indices
_.map(data, (d, i) => {
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
});
// TODO add some logic for targets heirarchy
cardsData.targets = _.keys(cardsData.targetIndex);
cardsData.targets.sort();
cardsData.yBucketSize = cardsData.targets.length;
cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length));
// Maximum number of buckets over x axis
cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length));
// Collect all values for each bucket from datapoints with similar target.
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
for(let i = 0; i < cardsData.targets.length; i++) {
let target = cardsData.targets[i];
@@ -292,15 +360,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
values: [],
multipleValues: false,
noColorDefined: false,
y: target,
x: -1,
};
// collect values from all timeseries with target
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
let s = data[cardsData.targetIndex[target][si]];
if (s.datapoints.length <= j) {
continue;
}
let datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
card.y = s.target;
}
card.values.push(datapoint[VALUE_INDEX]);
}
@@ -320,7 +392,9 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
if (cardsData.minValue > card.minValue)
cardsData.minValue = card.minValue;
if (card.x != -1) {
cardsData.cards.push(card);
}
}
}
+3 -3
View File
@@ -42,7 +42,7 @@ export class StatusHeatmapTooltip {
add() {
this.tooltip = d3.select("body")
.append("div")
.attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip");
.attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
}
destroy() {
@@ -79,7 +79,7 @@ export class StatusHeatmapTooltip {
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
<div class="status-heatmap-histogram"></div>`;
<div class="statusmap-histogram"></div>`;
if (this.panel.color.mode === 'discrete') {
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
@@ -94,7 +94,7 @@ export class StatusHeatmapTooltip {
name: <b>${y}</b> <br>
${statusesHtml}
<ul>
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}; padding: 1px; font-weight: bold; text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF">${v.tooltip}</li>`), "")}
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}" class="discrete-item">${v.tooltip}</li>`), "")}
</ul>
</div>`;
} else {