Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742e80d713 | ||
|
|
eb6f149f8b | ||
|
|
d4d297862e | ||
|
|
06cb92bbb8 | ||
|
|
5a758213c6 | ||
|
|
20691c9aae | ||
|
|
624be3450d | ||
|
|
63e36b1656 | ||
|
|
433a059bcf | ||
|
|
1862c5ac17 | ||
|
|
6665b7e18a | ||
|
|
4994f2f956 | ||
|
|
8d2039bf37 | ||
|
|
12492216d6 | ||
|
|
51623fdc72 | ||
|
|
27b674640b | ||
|
|
803504befd | ||
|
|
570fe11341 | ||
|
|
0537671450 | ||
|
|
089f42d940 | ||
|
|
2158753c5e | ||
|
|
dee408e7a3 | ||
|
|
c37d116550 | ||
|
|
a1a06ea78a | ||
|
|
f813f4cea7 | ||
|
|
3ffe5554f3 | ||
|
|
e1abad408e | ||
|
|
188d912653 | ||
|
|
8d917d0c1e | ||
|
|
f6973a81b8 | ||
|
|
f13adb369b | ||
|
|
cb5d353b36 | ||
|
|
e5249b8852 | ||
|
|
8dde6da21a | ||
|
|
3c3931b2c0 | ||
|
|
77ab254a5e | ||
|
|
12b974a2bc | ||
|
|
cc4b908afd | ||
|
|
cda8fdfe8d | ||
|
|
ec8f8e386a | ||
|
|
76dc89b2dc | ||
|
|
ca2fcb582a | ||
|
|
54539cb35f | ||
|
|
8e12d35da9 | ||
|
|
34f65b1455 | ||
|
|
e783d32efa | ||
|
|
a51f237291 | ||
|
|
46ba4be810 | ||
|
|
7ce802f3ef | ||
|
|
bccc3320a8 | ||
|
|
5d7cf54348 | ||
|
|
1f43ffe158 | ||
|
|
c7882402ce | ||
|
|
09cdd2f224 | ||
|
|
0a8463e2ed | ||
|
|
4d952a841b | ||
|
|
36e907f8b6 | ||
|
|
af55883905 | ||
|
|
afec01cb93 |
@@ -1,2 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.jshintrc
|
.jshintrc
|
||||||
|
.idea
|
||||||
|
.sass-cache
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -3,8 +3,18 @@ module.exports = (grunt) => {
|
|||||||
|
|
||||||
grunt.loadNpmTasks('grunt-execute');
|
grunt.loadNpmTasks('grunt-execute');
|
||||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||||
|
grunt.loadNpmTasks('grunt-notify');
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
|
notify: {
|
||||||
|
watch: {
|
||||||
|
options: {
|
||||||
|
message: 'grunt watch Complete',
|
||||||
|
title: 'flant-statusmap-panel rebuilded',
|
||||||
|
duration: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
clean: ['dist'],
|
clean: ['dist'],
|
||||||
|
|
||||||
@@ -17,7 +27,7 @@ module.exports = (grunt) => {
|
|||||||
},
|
},
|
||||||
pluginDef: {
|
pluginDef: {
|
||||||
expand: true,
|
expand: true,
|
||||||
src: ['plugin.json', 'README.md'],
|
src: ['plugin.json', 'README.md', 'CHANGELOG.md'],
|
||||||
dest: 'dist'
|
dest: 'dist'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,7 +35,7 @@ module.exports = (grunt) => {
|
|||||||
watch: {
|
watch: {
|
||||||
rebuild_all: {
|
rebuild_all: {
|
||||||
files: ['src/**/*', 'plugin.json'],
|
files: ['src/**/*', 'plugin.json'],
|
||||||
tasks: ['default'],
|
tasks: ['default', 'notify:watch'],
|
||||||
options: {
|
options: {
|
||||||
spawn: false,
|
spawn: false,
|
||||||
livereload: true
|
livereload: true
|
||||||
@@ -53,7 +63,8 @@ module.exports = (grunt) => {
|
|||||||
sass: {
|
sass: {
|
||||||
dist: {
|
dist: {
|
||||||
files: {
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,33 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Grouping values into rows and buckets using legend from query
|
* Grouping values into rows and buckets using legend from query
|
||||||
* User defined color mapping
|
* 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
|
* Increasing rows/buckets' interval for better visual representation
|
||||||
* Representing null values as empty bucket or zero value
|
* 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
|
### Supported environment
|
||||||
|
|
||||||
* Prometheus datasource
|
* Tested with datasources:
|
||||||
* Tested with Grafana 5.1.3
|
- Prometheus
|
||||||
|
- InfluxDB
|
||||||
|
- Mysql
|
||||||
|
* Tested with Grafana:
|
||||||
|
- 5.1.3
|
||||||
|
- 5.4.3
|
||||||
|
- 6.0.0
|
||||||
|
|
||||||
## Installation
|
## 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
|
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)
|
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.
|
of this repo and unpack it into /var/lib/grafana/plugins directory.
|
||||||
|
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
We had a desperate need to show a set of timeseries statuses over time period, so we can see
|
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
|
||||||
a history of changes for objects' statuses. Since we maintain a lot of Kubernetes clusters
|
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
|
(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
|
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
|
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.
|
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
|
_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
|
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.
|
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
|
## 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:
|
Requirements to store these statuses in metrics are as follows:
|
||||||
* metrics should have two values: `0` and `1`;
|
* metrics should have two values: `0` and `1`;
|
||||||
* there should be a label with status' value.
|
* 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
|
If there was no such status (`<STATUS_VALUE>`) during query's interval, Prometheus would
|
||||||
return nothing. Otherwise, status' value will be returned.
|
return nothing. Otherwise, status' value will be returned.
|
||||||
|
|
||||||
For example, if you have 5 statuses and a metric (`coffee_maker_status`) with 5 allowed
|
For example, if you have 5 types of statuses and a metric (`coffee_maker_status`) with 5
|
||||||
values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using the following rule:
|
allowed values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using following rule:
|
||||||
|
|
||||||
```
|
```
|
||||||
- record: coffee_maker_status:discrete
|
- 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
|
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.
|
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
|
### Panel
|
||||||
|
|
||||||
First of all, an individual query for each possible status' value should be created.
|
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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
||||||
|
|
||||||
|
|
||||||
### More options
|
### More options
|
||||||
|
|
||||||
|
#### Bucket
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__Multiple values__ checkbox specifies how they should be displayed:
|
__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.
|
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
||||||
__Rounding__ may be used to round edges.
|
__Rounding__ may be used to round edges.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Display
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
__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
|
## 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.
|
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
|
### CHANGELOG
|
||||||
|
|
||||||
- First public release
|
|
||||||
|
|
||||||
|
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -2,74 +2,186 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Features
|
## 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
|
* User defined color mapping
|
||||||
* Multiple values in bucket display in tooltip
|
* Multiple values in bucket are displayed via tooltip
|
||||||
* Interval shaping to better visual representation
|
* Increasing rows/buckets' interval for better visual representation
|
||||||
* Represent null values as empty bucket or as zero value
|
* 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
|
### Supported environment
|
||||||
|
|
||||||
* Prometheus datasource
|
* Tested with datasources:
|
||||||
* Tested with Grafana 5.1.3
|
- Prometheus
|
||||||
|
- InfluxDB
|
||||||
|
- Mysql
|
||||||
|
* Tested with Grafana:
|
||||||
|
- 5.1.3
|
||||||
|
- 5.4.3
|
||||||
|
- 6.0.0
|
||||||
|
|
||||||
## Installation
|
## 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
|
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
|
## Motivation
|
||||||
|
|
||||||
This plugin emerges from our needs to visually represent history of changes for a set of objects
|
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
|
||||||
with discrete statuses.
|
see a history of changes for objects' status. Since we maintain a lot of Kubernetes clusters
|
||||||
_Objects_ can be hosts, Kubernetes pods or coffee makers and _discrete statuses_ are a set
|
(and related infrastructure), our main cases for that are visualization of servers & Kubernetes
|
||||||
of predefined values: something like `ok` = 0, `off` = 1, `fail` = 2.
|
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
|
## 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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Next define color mapping for status values in __Discrete__ color mode.
|
Then, color mapping for status' values should be defined in __Discrete__ color mode:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
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.
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
||||||
|
|
||||||
|
|
||||||
### More options
|
### More options
|
||||||
|
|
||||||
|
#### Bucket
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__Multiple values__ check determine multiple values display mode. If check is unset then multiple values
|
__Multiple values__ checkbox specifies how they should be displayed:
|
||||||
for one bucket treated as error. If check is on then color for bucket determined
|
* If it's off, multiple values for one bucket are treated as error;
|
||||||
by value with least index in color mapping.
|
* If it's on, color for such bucket would be determined by the value having least index in color mapping.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__Min width__ and __spacing__ are determine minimal bucket width and spacing between buckets.
|
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
||||||
__Rounding__ is for round edges.
|
__Rounding__ may be used to round edges.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Display
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
__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
|
## 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.
|
To test and improve the plugin you can run Grafana instance in Docker using following command (in
|
||||||
This will expose the local plugin on your machine to the Grafana container.
|
the directory containing Statusmap plugin):
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
|
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
|
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
|
grunt watch
|
||||||
@@ -86,13 +199,11 @@ grunt watch
|
|||||||
|
|
||||||
## Acknowledgements
|
## 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
|
### CHANGELOG
|
||||||
|
|
||||||
- First public release
|
|
||||||
|
|
||||||
|
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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) {
|
System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic/index', 'app/core/core', 'app/core/utils/ticks'], function (_export, _context) {
|
||||||
"use strict";
|
"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) {
|
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
var legend = d3.select(legendElem.get(0));
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
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 legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
var rangeStep = 1;
|
var rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
|
||||||
if (rangeTo - rangeFrom > legendWidth) {
|
// width in pixels in legend space of unit segment in range space
|
||||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
var colorScale = getColorScale(colorScheme, maxValue, minValue);
|
var colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect")
|
||||||
return d * widthFactor;
|
// translate from range space into pixels
|
||||||
}).attr("y", 0).attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
// and shift all rectangles to the right by 10
|
||||||
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
.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);
|
return colorScale(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,22 +39,24 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
var legend = d3.select(legendElem.get(0));
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
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 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);
|
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
var opacityScale = getOpacityScale(options, maxValue, minValue);
|
var opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect")
|
||||||
return d * widthFactor;
|
// translate from range space into pixels
|
||||||
}).attr("y", 0).attr("width", rangeStep * widthFactor).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
|
// 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);
|
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
|
// calculate max width of tooltip and use it as width for each item
|
||||||
var textWidth = [];
|
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;
|
return d;
|
||||||
}).each(function (d, i) {
|
}).each(function (d, i) {
|
||||||
var thisWidth = this.getBBox().width;
|
var thisWidth = this.getBBox().width;
|
||||||
textWidth.push(thisWidth);
|
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]));
|
var legendWidth = Math.floor(_.min([graphWidth - 30, (_.max(textWidth) + 3) * valuesNumber]));
|
||||||
legendElem.attr("width", legendWidth);
|
legendElem.attr("width", legendWidth);
|
||||||
@@ -178,22 +178,18 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function drawSimpleColorLegend(elem, colorScale) {
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
var legendHeight = legendElem.attr("height");
|
var legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
if (legendWidth) {
|
if (legendWidth) {
|
||||||
var valuesNumber = Math.floor(legendWidth / 2);
|
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
var rangeStep = Math.floor(legendWidth / valuesNumber);
|
|
||||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
|
|
||||||
var legend = d3.select(legendElem.get(0));
|
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
||||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect").attr("x", function (d) {
|
|
||||||
return 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) {
|
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
||||||
return colorScale(d);
|
return colorScale(d);
|
||||||
});
|
});
|
||||||
@@ -202,10 +198,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function drawSimpleOpacityLegend(elem, options) {
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
var graphElem = $(elem);
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
var legend = d3.select(legendElem.get(0));
|
|
||||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
var legendHeight = legendElem.attr("height");
|
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]);
|
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([0, legendWidth]).range([0, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rangeStep = 10;
|
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
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;
|
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);
|
return legendOpacityScale(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -329,11 +322,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
mod = angular.module('grafana.directives');
|
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 () {
|
mod.directive('optionsColorLegend', function () {
|
||||||
return {
|
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 () {
|
mod.directive('statusHeatmapLegend', function () {
|
||||||
return {
|
return {
|
||||||
@@ -384,6 +377,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
if (!ctrl.panel.legend.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||||
var rangeFrom = ctrl.cardsData.minValue;
|
var rangeFrom = ctrl.cardsData.minValue;
|
||||||
var rangeTo = ctrl.cardsData.maxValue;
|
var rangeTo = ctrl.cardsData.maxValue;
|
||||||
|
|||||||
@@ -101,9 +101,33 @@ System.register(["lodash"], function (_export, _context) {
|
|||||||
value: function getBucketColor(values) {
|
value: function getBucketColor(values) {
|
||||||
var thresholds = this.panel.color.thresholds;
|
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 i = 0; i < thresholds.length; i++) {
|
||||||
for (var j = 0; j < values.length; j++) {
|
for (var _j = 0; _j < values.length; _j++) {
|
||||||
if (values[j] == thresholds[i].value) {
|
if (values[_j] == thresholds[i].value) {
|
||||||
return this.getDiscreteColor(i);
|
return this.getDiscreteColor(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +159,7 @@ System.register(["lodash"], function (_export, _context) {
|
|||||||
key: "getMatchedThreshold",
|
key: "getMatchedThreshold",
|
||||||
value: function getMatchedThreshold(value) {
|
value: function getMatchedThreshold(value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (this.panel.color.nullPointMode == 'as empty') {
|
if (this.panel.nullPointMode == 'as empty') {
|
||||||
// FIXME: make this explicit for user
|
// FIXME: make this explicit for user
|
||||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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 */
|
||||||
@@ -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 {
|
.status-heatmap-canvas-wrapper {
|
||||||
cursor: crosshair; }
|
cursor: crosshair; }
|
||||||
|
.status-heatmap-canvas-wrapper .datapoints-warning {
|
||||||
|
pointer-events: none; }
|
||||||
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
||||||
background-color: white;
|
background-color: #e9edf2;
|
||||||
|
color: #52545c;
|
||||||
padding: 1px; }
|
padding: 1px; }
|
||||||
|
|
||||||
.status-heatmap-panel {
|
.status-heatmap-panel {
|
||||||
@@ -18,17 +21,22 @@
|
|||||||
.status-heatmap-panel svg rect {
|
.status-heatmap-panel svg rect {
|
||||||
pointer-events: visiblePainted; }
|
pointer-events: visiblePainted; }
|
||||||
|
|
||||||
.status-heatmap-tooltip {
|
.statusmap-tooltip {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background-color: #dde4ed;
|
background-color: #dde4ed;
|
||||||
color: #52545c; }
|
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; }
|
fill: #767980; }
|
||||||
|
|
||||||
.status-heatmap-crosshair line {
|
.status-heatmap-crosshair line {
|
||||||
stroke: #b30000;
|
stroke: #9e2f22;
|
||||||
stroke-width: 1; }
|
stroke-width: 1; }
|
||||||
|
|
||||||
.status-heatmap-selection {
|
.status-heatmap-selection {
|
||||||
@@ -57,4 +65,4 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
stroke: #767980; }
|
stroke: #767980; }
|
||||||
|
|
||||||
/*# sourceMappingURL=status-heatmap.css.map */
|
/*# sourceMappingURL=statusmap.light.css.map */
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 64 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<div class="status-heatmap-wrapper">
|
<div class="status-heatmap-wrapper">
|
||||||
<div class="status-heatmap-canvas-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.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>
|
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
'use strict';
|
'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";
|
"use strict";
|
||||||
|
|
||||||
var StatusHeatmapCtrl;
|
var loadPluginCss, StatusHeatmapCtrl;
|
||||||
return {
|
return {
|
||||||
setters: [function (_color_legend) {}, function (_status_heatmap_ctrl) {
|
setters: [function (_appPluginsSdk) {
|
||||||
|
loadPluginCss = _appPluginsSdk.loadPluginCss;
|
||||||
|
}, function (_status_heatmap_ctrl) {
|
||||||
StatusHeatmapCtrl = _status_heatmap_ctrl.StatusHeatmapCtrl;
|
StatusHeatmapCtrl = _status_heatmap_ctrl.StatusHeatmapCtrl;
|
||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
|
|
||||||
|
loadPluginCss({
|
||||||
|
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
|
||||||
|
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
|
||||||
|
});
|
||||||
|
|
||||||
_export('PanelCtrl', StatusHeatmapCtrl);
|
_export('PanelCtrl', StatusHeatmapCtrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"]}
|
||||||
@@ -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"]}
|
||||||
@@ -55,40 +55,70 @@
|
|||||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
<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-label width-2">{{ $index }}</label>
|
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</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>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
<div class="gf-form">
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
<label class="gf-form-label width-2">{{ $index }}</label>
|
||||||
Add trafic lights
|
</div>
|
||||||
</a>
|
<div class="gf-form">
|
||||||
</label>
|
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
||||||
Add new status
|
<label class="gf-form-label query-keyword">
|
||||||
</a>
|
fill
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
@@ -118,7 +148,16 @@
|
|||||||
label="Show tooltip"
|
label="Show tooltip"
|
||||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</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">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Bucket</h5>
|
<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>
|
<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>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Spacing</label>
|
<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="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
<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>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Rounding</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,13 @@
|
|||||||
|
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"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": "Color mapping", "path": "img/color-mapping.png"},
|
||||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||||
],
|
],
|
||||||
"version": "0.0.1",
|
"version": "0.1.1",
|
||||||
"updated": "2018-09-17"
|
"updated": "2019-03-22"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use strict';
|
'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";
|
"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) {
|
function link(scope, elem, attrs, ctrl) {
|
||||||
var data = void 0,
|
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
|
// $heatmap is JQuery object, but heatmap is D3
|
||||||
var $heatmap = elem.find('.status-heatmap-panel');
|
var $heatmap = elem.find('.status-heatmap-panel');
|
||||||
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||||
|
var annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||||
|
|
||||||
var width = void 0,
|
var width = void 0,
|
||||||
height = void 0,
|
height = void 0,
|
||||||
@@ -26,7 +27,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
chartBottom = void 0,
|
chartBottom = void 0,
|
||||||
yAxisWidth = void 0,
|
yAxisWidth = void 0,
|
||||||
xAxisHeight = void 0,
|
xAxisHeight = void 0,
|
||||||
cardSpacing = void 0,
|
cardVSpacing = void 0,
|
||||||
|
cardHSpacing = void 0,
|
||||||
cardRound = void 0,
|
cardRound = void 0,
|
||||||
cardWidth = void 0,
|
cardWidth = void 0,
|
||||||
cardHeight = 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 text.getBBox().width;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return max_text_width;
|
return Math.ceil(max_text_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getXAxisHeight(elem) {
|
function getXAxisHeight(elem) {
|
||||||
@@ -121,9 +123,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
function getYScale(ticks) {
|
function getYScale(ticks) {
|
||||||
var range = [];
|
var range = [];
|
||||||
var step = chartHeight / ticks.length;
|
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++) {
|
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);
|
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) {
|
function getYAxisScale(ticks) {
|
||||||
var range = [];
|
var range = [];
|
||||||
var step = chartHeight / ticks.length;
|
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++) {
|
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);
|
return d3.scaleOrdinal().domain(ticks).range(range);
|
||||||
}
|
}
|
||||||
@@ -149,6 +153,16 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
ticks = [''];
|
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);
|
var yAxisScale = getYAxisScale(ticks);
|
||||||
scope.yScale = yScale = getYScale(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;
|
chartTop = margin.top;
|
||||||
chartBottom = chartTop + chartHeight;
|
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;
|
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||||
|
|
||||||
// calculate yOffset for YAxis
|
// calculate yOffset for YAxis
|
||||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||||
yOffset = cardHeight / 2;
|
yOffset = cardHeight / 2;
|
||||||
|
|
||||||
addYAxis();
|
addYAxis();
|
||||||
@@ -229,9 +244,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||||
chartWidth = width - yAxisWidth - margin.right;
|
chartWidth = width - yAxisWidth - margin.right;
|
||||||
|
|
||||||
|
// TODO allow per-y cardWidth!
|
||||||
// we need to fill chartWidth with xBucketSize cards.
|
// we need to fill chartWidth with xBucketSize cards.
|
||||||
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
|
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
|
||||||
cardWidth = xGridSize - cardSpacing;
|
cardWidth = xGridSize - cardHSpacing;
|
||||||
|
|
||||||
addXAxis();
|
addXAxis();
|
||||||
xAxisHeight = getXAxisHeight(heatmap);
|
xAxisHeight = getXAxisHeight(heatmap);
|
||||||
@@ -274,6 +290,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
resetCardHighLight(event);
|
resetCardHighLight(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_renderAnnotations();
|
||||||
|
|
||||||
ctrl.events.emit('render-complete', {
|
ctrl.events.emit('render-complete', {
|
||||||
"chartWidth": chartWidth
|
"chartWidth": chartWidth
|
||||||
});
|
});
|
||||||
@@ -321,7 +339,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
var cx = xScale(d.x);
|
var cx = xScale(d.x);
|
||||||
|
|
||||||
if (cx - cardWidth / 2 < 0) {
|
if (cx - cardWidth / 2 < 0) {
|
||||||
x = yAxisWidth + cardSpacing / 2;
|
x = yAxisWidth + cardHSpacing / 2;
|
||||||
} else {
|
} else {
|
||||||
x = yAxisWidth + cx - cardWidth / 2;
|
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) {
|
if (cx < cardWidth / 2) {
|
||||||
// Center should not exceed half of card.
|
// Center should not exceed half of card.
|
||||||
// Cut card to the left to prevent overlay of y axis.
|
// 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;
|
w = cutted_width > 0 ? cutted_width : 0;
|
||||||
} else if (chartWidth - cx < cardWidth / 2) {
|
} else if (chartWidth - cx < cardWidth / 2) {
|
||||||
// Cut card to the right to prevent overlay of right graph edge.
|
// 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 {
|
} else {
|
||||||
w = cardWidth;
|
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
|
// Card width should be MIN_CARD_SIZE at least
|
||||||
w = Math.max(w, MIN_CARD_SIZE);
|
w = Math.max(w, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardHSpacing == 0) {
|
||||||
|
w = w + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardY(d) {
|
function getCardY(d) {
|
||||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing / 2;
|
return yScale(d.y) + chartTop - cardHeight - cardVSpacing / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardHeight(d) {
|
function getCardHeight(d) {
|
||||||
var ys = yScale(d.y);
|
var ys = yScale(d.y);
|
||||||
var y = ys + chartTop - cardHeight - cardSpacing / 2;
|
var y = ys + chartTop - cardHeight - cardVSpacing / 2;
|
||||||
var h = cardHeight;
|
var h = cardHeight;
|
||||||
|
|
||||||
// Cut card height to prevent overlay
|
// Cut card height to prevent overlay
|
||||||
if (y < chartTop) {
|
if (y < chartTop) {
|
||||||
h = ys - cardSpacing / 2;
|
h = ys - cardVSpacing / 2;
|
||||||
} else if (ys > chartBottom) {
|
} else if (ys > chartBottom) {
|
||||||
h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
} else if (y + cardHeight > chartBottom) {
|
} 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
|
// Card height should be MIN_CARD_SIZE at least
|
||||||
h = Math.max(h, MIN_CARD_SIZE);
|
h = Math.max(h, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardVSpacing == 0) {
|
||||||
|
h = h + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +478,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
|
//annotationTooltip.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(event) {
|
function onMouseMove(event) {
|
||||||
@@ -463,6 +490,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
// Clear crosshair and tooltip
|
// Clear crosshair and tooltip
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
tooltip.destroy();
|
tooltip.destroy();
|
||||||
|
annotationTooltip.destroy();
|
||||||
|
|
||||||
selection.x2 = limitSelection(event.offsetX);
|
selection.x2 = limitSelection(event.offsetX);
|
||||||
drawSelection(selection.x1, selection.x2);
|
drawSelection(selection.x1, selection.x2);
|
||||||
@@ -470,6 +498,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
emitGraphHoverEvet(event);
|
emitGraphHoverEvet(event);
|
||||||
drawCrosshair(event.offsetX);
|
drawCrosshair(event.offsetX);
|
||||||
tooltip.show(event); //, data);
|
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("mousedown", onMouseDown);
|
||||||
$heatmap.on("mousemove", onMouseMove);
|
$heatmap.on("mousemove", onMouseMove);
|
||||||
$heatmap.on("mouseleave", onMouseLeave);
|
$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);
|
_export('default', link);
|
||||||
@@ -622,10 +694,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
|
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
|
||||||
}, function (_tooltip) {
|
}, function (_tooltip) {
|
||||||
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
|
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
|
||||||
|
}, function (_annotations) {
|
||||||
|
AnnotationTooltip = _annotations.AnnotationTooltip;
|
||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
MIN_CARD_SIZE = 5;
|
MIN_CARD_SIZE = 5;
|
||||||
CARD_SPACING = 2;
|
CARD_H_SPACING = 2;
|
||||||
|
CARD_V_SPACING = 2;
|
||||||
CARD_ROUND = 0;
|
CARD_ROUND = 0;
|
||||||
DATA_RANGE_WIDING_FACTOR = 1.2;
|
DATA_RANGE_WIDING_FACTOR = 1.2;
|
||||||
DEFAULT_X_TICK_SIZE_PX = 100;
|
DEFAULT_X_TICK_SIZE_PX = 100;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use strict';
|
'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";
|
"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) {
|
function _classCallCheck(instance, Constructor) {
|
||||||
if (!(instance instanceof 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;
|
contextSrv = _appCoreCore.contextSrv;
|
||||||
}, function (_appCoreUtilsKbn) {
|
}, function (_appCoreUtilsKbn) {
|
||||||
kbn = _appCoreUtilsKbn.default;
|
kbn = _appCoreUtilsKbn.default;
|
||||||
}, function (_rendering) {
|
}, function (_color_legend) {}, function (_rendering) {
|
||||||
rendering = _rendering.default;
|
rendering = _rendering.default;
|
||||||
}, function (_options_editor) {
|
}, function (_options_editor) {
|
||||||
statusHeatmapOptionsEditor = _options_editor.statusHeatmapOptionsEditor;
|
statusHeatmapOptionsEditor = _options_editor.statusHeatmapOptionsEditor;
|
||||||
}, function (_color_mode_discrete) {
|
}, function (_color_mode_discrete) {
|
||||||
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
|
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
|
||||||
}, function (_cssStatusHeatmapCss) {}],
|
}],
|
||||||
execute: function () {
|
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';
|
CANVAS = 'CANVAS';
|
||||||
SVG = 'SVG';
|
SVG = 'SVG';
|
||||||
VALUE_INDEX = 0;
|
VALUE_INDEX = 0;
|
||||||
@@ -71,7 +96,8 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cardMinWidth: 5,
|
cardMinWidth: 5,
|
||||||
cardSpacing: 2,
|
cardVSpacing: 2,
|
||||||
|
cardHSpacing: 2,
|
||||||
cardRound: null
|
cardRound: null
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -97,6 +123,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
},
|
},
|
||||||
// how null points should be handled
|
// how null points should be handled
|
||||||
nullPointMode: 'as empty',
|
nullPointMode: 'as empty',
|
||||||
|
yAxisSort: 'metrics',
|
||||||
highlightCards: true,
|
highlightCards: true,
|
||||||
useMax: 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' },
|
{ 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)
|
// 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'];
|
colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||||
opacityScales = ['linear', 'sqrt'];
|
opacityScales = ['linear', 'sqrt'];
|
||||||
|
|
||||||
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
|
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
|
||||||
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
|
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
|
||||||
|
|
||||||
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv) {
|
/** @ngInject */
|
||||||
|
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||||
_classCallCheck(this, StatusHeatmapCtrl);
|
_classCallCheck(this, StatusHeatmapCtrl);
|
||||||
|
|
||||||
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
|
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
|
||||||
|
|
||||||
_this.onRenderComplete = function (data) {
|
_this.onRenderComplete = function (data) {
|
||||||
_this.graph.chartWidth = data.chartWidth;
|
_this.graph.chartWidth = data.chartWidth;
|
||||||
|
_this.renderingCompleted();
|
||||||
};
|
};
|
||||||
|
|
||||||
_this.calculateInterval = function () {
|
_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 chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
|
||||||
|
|
||||||
var minCardWidth = _this.panel.cards.cardMinWidth;
|
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 maxCardsCount = Math.ceil((chartWidth - minCardWidth) / (minCardWidth + minSpacing));
|
||||||
|
|
||||||
var intervalMs = void 0;
|
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.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.onDataReceived = function (dataList) {
|
||||||
_this.data = dataList;
|
_this.data = dataList;
|
||||||
_this.cardsData = _this.convertToCards(_this.data);
|
_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 () {
|
_this.onInitEditMode = function () {
|
||||||
@@ -206,6 +273,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
|
|
||||||
_this.onDataError = function () {
|
_this.onDataError = function () {
|
||||||
_this.data = [];
|
_this.data = [];
|
||||||
|
_this.annotations = [];
|
||||||
_this.render();
|
_this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,6 +291,11 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
_this.render();
|
_this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_this.onEditorRemoveThresholds = function () {
|
||||||
|
_this.panel.color.thresholds = [];
|
||||||
|
_this.render();
|
||||||
|
};
|
||||||
|
|
||||||
_this.onEditorAddThreeLights = function () {
|
_this.onEditorAddThreeLights = function () {
|
||||||
_this.panel.color.thresholds.push({ color: "red", value: 2, tooltip: "error" });
|
_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: "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.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) {
|
_this.link = function (scope, elem, attrs, ctrl) {
|
||||||
rendering(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,
|
maxValue: 0,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
multipleValues: false,
|
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) {
|
if (!data || data.length == 0) {
|
||||||
return cardsData;
|
return cardsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect uniq targets and their indexes in data array
|
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||||
cardsData.targetIndex = {};
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
// collect uniq targets and their indices
|
||||||
var ts = data[i];
|
_.map(data, function (d, i) {
|
||||||
var target = ts.target;
|
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i);
|
||||||
if (cardsData.targetIndex[target] == undefined) {
|
});
|
||||||
cardsData.targetIndex[target] = [];
|
|
||||||
}
|
|
||||||
cardsData.targetIndex[target].push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add some logic for targets heirarchy
|
// TODO add some logic for targets heirarchy
|
||||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||||
cardsData.targets.sort();
|
|
||||||
cardsData.yBucketSize = cardsData.targets.length;
|
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;
|
return d.datapoints.length;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Collect all values for each bucket from datapoints with similar target.
|
// Collect all values for each bucket from datapoints with similar target.
|
||||||
for (var _i = 0; _i < cardsData.targets.length; _i++) {
|
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
|
||||||
var _target = cardsData.targets[_i];
|
for (var i = 0; i < cardsData.targets.length; i++) {
|
||||||
|
var target = cardsData.targets[i];
|
||||||
|
|
||||||
for (var j = 0; j < cardsData.xBucketSize; j++) {
|
for (var j = 0; j < cardsData.xBucketSize; j++) {
|
||||||
var card = {
|
var card = {
|
||||||
id: _i * cardsData.xBucketSize + j,
|
id: i * cardsData.xBucketSize + j,
|
||||||
values: [],
|
values: [],
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false
|
noColorDefined: false,
|
||||||
|
y: target,
|
||||||
|
x: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
// collect values from all timeseries with target
|
// collect values from all timeseries with target
|
||||||
for (var si = 0; si < cardsData.targetIndex[_target].length; si++) {
|
for (var si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||||
var s = data[cardsData.targetIndex[_target][si]];
|
var s = data[cardsData.targetIndex[target][si]];
|
||||||
|
if (s.datapoints.length <= j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var datapoint = s.datapoints[j];
|
var datapoint = s.datapoints[j];
|
||||||
if (card.values.length === 0) {
|
if (card.values.length === 0) {
|
||||||
card.x = datapoint[TIME_INDEX];
|
card.x = datapoint[TIME_INDEX];
|
||||||
card.y = s.target;
|
|
||||||
}
|
}
|
||||||
card.values.push(datapoint[VALUE_INDEX]);
|
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;
|
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-received', _this.onDataReceived);
|
||||||
_this.events.on('data-snapshot-load', _this.onDataReceived);
|
_this.events.on('data-snapshot-load', _this.onDataReceived);
|
||||||
_this.events.on('data-error', _this.onDataError);
|
_this.events.on('data-error', _this.onDataError);
|
||||||
_this.events.on('init-edit-mode', _this.onInitEditMode);
|
_this.events.on('init-edit-mode', _this.onInitEditMode);
|
||||||
_this.events.on('render', _this.onRender);
|
_this.events.on('render', _this.onRender);
|
||||||
_this.events.on('refresh', _this.postRefresh);
|
_this.events.on('refresh', _this.postRefresh);
|
||||||
|
// custom event from rendering.js
|
||||||
_this.events.on('render-complete', _this.onRenderComplete);
|
_this.events.on('render-complete', _this.onRenderComplete);
|
||||||
return _this;
|
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
|
// override calculateInterval for discrete color mode
|
||||||
|
|
||||||
|
|
||||||
|
/* https://ethanschoonover.com/solarized/ */
|
||||||
|
|
||||||
|
|
||||||
// group values into buckets by target
|
// group values into buckets by target
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
|
|||||||
}, {
|
}, {
|
||||||
key: 'add',
|
key: 'add',
|
||||||
value: function 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',
|
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 tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
var time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
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') {
|
if (this.panel.color.mode === 'discrete') {
|
||||||
var statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
var statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
||||||
@@ -139,7 +139,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
|
|||||||
statusesHtml = "statuses:";
|
statusesHtml = "statuses:";
|
||||||
}
|
}
|
||||||
tooltipHtml += '\n <div>\n name: <b>' + y + '</b> <br>\n ' + statusesHtml + '\n <ul>\n ' + _.join(_.map(statuses, function (v) {
|
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>';
|
}), "") + '\n </ul>\n </div>';
|
||||||
} else {
|
} else {
|
||||||
if (values.length === 1) {
|
if (values.length === 1) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "status-hetamap",
|
"name": "flant-statusmap-panel",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "Status Heatmap panel plugin for grafana",
|
"description": "Grafana panel plugin to visualize status of multiple objects over time",
|
||||||
"main": "README.md",
|
"main": "README.md",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "grunt"
|
"build": "grunt"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/flant/grafana-status-heatmap.git"
|
"url": "https://github.com/flant/grafana-statusmap.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel": "^6.23.0",
|
"babel": "^6.23.0",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"grunt-contrib-uglify": "^3.0.1",
|
"grunt-contrib-uglify": "^3.0.1",
|
||||||
"grunt-contrib-watch": "^1.0.0",
|
"grunt-contrib-watch": "^1.0.0",
|
||||||
"grunt-execute": "^0.2.2",
|
"grunt-execute": "^0.2.2",
|
||||||
|
"grunt-notify": "^0.4.5",
|
||||||
"grunt-systemjs-builder": "^1.0.0",
|
"grunt-systemjs-builder": "^1.0.0",
|
||||||
"load-grunt-tasks": "^3.5.2"
|
"load-grunt-tasks": "^3.5.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import {tickStep} from 'app/core/utils/ticks';
|
|||||||
|
|
||||||
let mod = angular.module('grafana.directives');
|
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() {
|
mod.directive('optionsColorLegend', function() {
|
||||||
return {
|
return {
|
||||||
@@ -45,7 +45,7 @@ mod.directive('optionsColorLegend', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heatmap legend with scale values.
|
* Graph legend with values.
|
||||||
*/
|
*/
|
||||||
mod.directive('statusHeatmapLegend', function() {
|
mod.directive('statusHeatmapLegend', function() {
|
||||||
return {
|
return {
|
||||||
@@ -62,6 +62,9 @@ mod.directive('statusHeatmapLegend', function() {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
if (!ctrl.panel.legend.show) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||||
let rangeFrom = ctrl.cardsData.minValue;
|
let rangeFrom = ctrl.cardsData.minValue;
|
||||||
let rangeTo = ctrl.cardsData.maxValue;
|
let rangeTo = ctrl.cardsData.maxValue;
|
||||||
@@ -89,18 +92,12 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
|
|||||||
let legend = d3.select(legendElem.get(0));
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
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 legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
let rangeStep = 1;
|
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
|
||||||
if (rangeTo - rangeFrom > legendWidth) {
|
// width in pixels in legend space of unit segment in range space
|
||||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
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")
|
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||||
.data(valuesRange)
|
.data(valuesRange)
|
||||||
.enter().append("rect")
|
.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("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("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", d => colorScale(d));
|
.attr("fill", d => colorScale(d));
|
||||||
@@ -123,25 +123,25 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
|||||||
let legend = d3.select(legendElem.get(0));
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
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 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);
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||||
.data(valuesRange)
|
.data(valuesRange)
|
||||||
.enter().append("rect")
|
.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("y", 0)
|
||||||
.attr("width", rangeStep * widthFactor)
|
// rectangles are slightly overlaped to prevent gaps
|
||||||
|
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", options.cardColor)
|
.attr("fill", options.cardColor)
|
||||||
@@ -168,14 +168,14 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
|
|||||||
legend.selectAll(".hidden-texts")
|
legend.selectAll(".hidden-texts")
|
||||||
.data(tooltips)
|
.data(tooltips)
|
||||||
.enter().append("text")
|
.enter().append("text")
|
||||||
.attr("class", "axis tick")
|
.attr("class", "axis tick hidden-texts")
|
||||||
.attr("font-family", "sans-serif")
|
.attr("font-family", "sans-serif")
|
||||||
.text(d => d)
|
.text(d => d)
|
||||||
.each(function(d,i) {
|
.each(function(d,i) {
|
||||||
let thisWidth = this.getBBox().width;
|
let thisWidth = this.getBBox().width;
|
||||||
textWidth.push(thisWidth);
|
textWidth.push(thisWidth);
|
||||||
this.remove(); // remove them just after displaying them
|
|
||||||
});
|
});
|
||||||
|
legend.selectAll(".hidden-texts").remove();
|
||||||
|
|
||||||
let legendWidth = Math.floor(_.min([
|
let legendWidth = Math.floor(_.min([
|
||||||
graphWidth - 30,
|
graphWidth - 30,
|
||||||
@@ -289,23 +289,21 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
|||||||
|
|
||||||
function drawSimpleColorLegend(elem, colorScale) {
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
if (legendWidth) {
|
if (legendWidth) {
|
||||||
let valuesNumber = Math.floor(legendWidth / 2);
|
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
|
|
||||||
let legend = d3.select(legendElem.get(0));
|
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
legendRects.enter().append("rect")
|
|
||||||
.attr("x", d => d)
|
.attr("x", d => d)
|
||||||
.attr("y", 0)
|
.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("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", d => colorScale(d));
|
.attr("fill", d => colorScale(d));
|
||||||
@@ -314,10 +312,9 @@ function drawSimpleColorLegend(elem, colorScale) {
|
|||||||
|
|
||||||
function drawSimpleOpacityLegend(elem, options) {
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
let graphElem = $(elem);
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
let legend = d3.select(legendElem.get(0));
|
|
||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
@@ -333,14 +330,14 @@ function drawSimpleOpacityLegend(elem, options) {
|
|||||||
.range([0, 1]);
|
.range([0, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rangeStep = 10;
|
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect")
|
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||||
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
.attr("x", d => d)
|
.attr("x", d => d)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", rangeStep)
|
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", options.cardColor)
|
.attr("fill", options.cardColor)
|
||||||
|
|||||||
@@ -61,6 +61,30 @@ export class ColorModeDiscrete {
|
|||||||
getBucketColor(values) {
|
getBucketColor(values) {
|
||||||
let thresholds = this.panel.color.thresholds;
|
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 i = 0; i < thresholds.length; i++) {
|
||||||
for (let j = 0; j < values.length; j++) {
|
for (let j = 0; j < values.length; j++) {
|
||||||
if (values[j] == thresholds[i].value) {
|
if (values[j] == thresholds[i].value) {
|
||||||
@@ -93,7 +117,7 @@ export class ColorModeDiscrete {
|
|||||||
|
|
||||||
getMatchedThreshold(value) {
|
getMatchedThreshold(value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (this.panel.color.nullPointMode == 'as empty') {
|
if (this.panel.nullPointMode == 'as empty') {
|
||||||
// FIXME: make this explicit for user
|
// FIXME: make this explicit for user
|
||||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
$font-size-sm: 12px !default;
|
|
||||||
|
|
||||||
|
|
||||||
.status-heatmap-canvas-wrapper {
|
.status-heatmap-canvas-wrapper {
|
||||||
// position: relative;
|
// position: relative;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
|
||||||
|
// allow tooltips on hover of warning text
|
||||||
|
.datapoints-warning {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.datapoints-warning span {
|
.datapoints-warning span {
|
||||||
background-color: white;
|
background-color: $warning-bg;
|
||||||
|
color: $text-color;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,14 +19,14 @@ $font-size-sm: 12px !default;
|
|||||||
|
|
||||||
.axis .tick {
|
.axis .tick {
|
||||||
text {
|
text {
|
||||||
fill: #52545c;
|
fill: $text-color;
|
||||||
color: #52545c;
|
color: $text-color;
|
||||||
font-size: 11px;
|
font-size: $font-size-tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
line {
|
line {
|
||||||
opacity: 0.4;
|
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;
|
white-space: nowrap;
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
background-color: #dde4ed;
|
background-color: $graph-tooltip-bg;
|
||||||
color: #52545c;
|
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 {
|
.statusmap-histogram rect {
|
||||||
fill: #767980;
|
fill: $text-color-weak;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-heatmap-crosshair {
|
.status-heatmap-crosshair {
|
||||||
line {
|
line {
|
||||||
stroke: darken(red,15%);
|
stroke: darken($red,15%);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,14 +71,11 @@ $font-size-sm: 12px !default;
|
|||||||
stroke: rgba(102, 102, 102, 0.8);
|
stroke: rgba(102, 102, 102, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.status-heatmap-legend-wrapper {
|
.status-heatmap-legend-wrapper {
|
||||||
// @include clearfix();
|
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
//padding-top: 10px;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
//width: 100%;
|
|
||||||
//max-width: 1300px;
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
float: left;
|
float: left;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -85,19 +92,19 @@ $font-size-sm: 12px !default;
|
|||||||
|
|
||||||
.axis .tick {
|
.axis .tick {
|
||||||
text {
|
text {
|
||||||
fill: #52545c;
|
fill: $text-color;
|
||||||
color: #52545c;
|
color: $text-color;
|
||||||
font-size: 11px;
|
font-size: $font-size-tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
line {
|
line {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
stroke: #767980;
|
stroke: $text-color-weak;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain {
|
.domain {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
stroke: #767980;
|
stroke: $text-color-weak;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
$font-size-sm: 12px !default;
|
||||||
|
$font-size-tick: 11px;
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import "variables";
|
||||||
|
@import "variables.dark";
|
||||||
|
@import "statusmap"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import "variables";
|
||||||
|
@import "variables.light";
|
||||||
|
@import "statusmap"
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<div class="status-heatmap-wrapper">
|
<div class="status-heatmap-wrapper">
|
||||||
<div class="status-heatmap-canvas-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.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>
|
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import './color_legend';
|
import {loadPluginCss} from 'app/plugins/sdk';
|
||||||
import { StatusHeatmapCtrl } from './status_heatmap_ctrl';
|
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 {
|
export {
|
||||||
StatusHeatmapCtrl as PanelCtrl
|
StatusHeatmapCtrl as PanelCtrl
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,40 +55,70 @@
|
|||||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
<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-label width-2">{{ $index }}</label>
|
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</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>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
<div class="gf-form">
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
<label class="gf-form-label width-2">{{ $index }}</label>
|
||||||
Add trafic lights
|
</div>
|
||||||
</a>
|
<div class="gf-form">
|
||||||
</label>
|
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
||||||
Add new status
|
<label class="gf-form-label query-keyword">
|
||||||
</a>
|
fill
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
@@ -118,7 +148,16 @@
|
|||||||
label="Show tooltip"
|
label="Show tooltip"
|
||||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</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">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Bucket</h5>
|
<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>
|
<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>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Spacing</label>
|
<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="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
<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>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Rounding</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,13 @@
|
|||||||
|
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"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": "Color mapping", "path": "img/color-mapping.png"},
|
||||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||||
],
|
],
|
||||||
"version": "0.0.1",
|
"version": "0.1.1",
|
||||||
"updated": "2018-09-17"
|
"updated": "2019-03-22"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks
|
|||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
||||||
import {StatusHeatmapTooltip} from './tooltip';
|
import {StatusHeatmapTooltip} from './tooltip';
|
||||||
|
import {AnnotationTooltip} from './annotations';
|
||||||
|
|
||||||
let MIN_CARD_SIZE = 5,
|
let MIN_CARD_SIZE = 5,
|
||||||
CARD_SPACING = 2,
|
CARD_H_SPACING = 2,
|
||||||
|
CARD_V_SPACING = 2,
|
||||||
CARD_ROUND = 0,
|
CARD_ROUND = 0,
|
||||||
DATA_RANGE_WIDING_FACTOR = 1.2,
|
DATA_RANGE_WIDING_FACTOR = 1.2,
|
||||||
DEFAULT_X_TICK_SIZE_PX = 100,
|
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
|
// $heatmap is JQuery object, but heatmap is D3
|
||||||
let $heatmap = elem.find('.status-heatmap-panel');
|
let $heatmap = elem.find('.status-heatmap-panel');
|
||||||
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||||
|
let annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||||
|
|
||||||
let width, height,
|
let width, height,
|
||||||
yScale, xScale,
|
yScale, xScale,
|
||||||
chartWidth, chartHeight,
|
chartWidth, chartHeight,
|
||||||
chartTop, chartBottom,
|
chartTop, chartBottom,
|
||||||
yAxisWidth, xAxisHeight,
|
yAxisWidth, xAxisHeight,
|
||||||
cardSpacing, cardRound,
|
cardVSpacing, cardHSpacing, cardRound,
|
||||||
cardWidth, cardHeight,
|
cardWidth, cardHeight,
|
||||||
colorScale, opacityScale,
|
colorScale, opacityScale,
|
||||||
mouseUpHandler,
|
mouseUpHandler,
|
||||||
@@ -76,7 +79,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
return text.getBBox().width;
|
return text.getBBox().width;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return max_text_width;
|
return Math.ceil(max_text_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getXAxisHeight(elem) {
|
function getXAxisHeight(elem) {
|
||||||
@@ -129,9 +132,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function getYScale(ticks) {
|
function getYScale(ticks) {
|
||||||
let range = [];
|
let range = [];
|
||||||
let step = chartHeight / ticks.length;
|
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++) {
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i);
|
range.push(step * (i+1));
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal()
|
return d3.scaleOrdinal()
|
||||||
.domain(ticks)
|
.domain(ticks)
|
||||||
@@ -142,9 +146,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function getYAxisScale(ticks) {
|
function getYAxisScale(ticks) {
|
||||||
let range = [];
|
let range = [];
|
||||||
let step = chartHeight / ticks.length;
|
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++) {
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i - yOffset);
|
range.push(step * i + yOffset);
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal()
|
return d3.scaleOrdinal()
|
||||||
.domain(ticks)
|
.domain(ticks)
|
||||||
@@ -159,6 +164,12 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
ticks = [''];
|
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);
|
let yAxisScale = getYAxisScale(ticks);
|
||||||
scope.yScale = yScale = getYScale(ticks);
|
scope.yScale = yScale = getYScale(ticks);
|
||||||
|
|
||||||
@@ -231,12 +242,13 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
chartTop = margin.top;
|
chartTop = margin.top;
|
||||||
chartBottom = chartTop + chartHeight;
|
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;
|
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||||
|
|
||||||
// calculate yOffset for YAxis
|
// calculate yOffset for YAxis
|
||||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||||
yOffset = cardHeight / 2;
|
yOffset = cardHeight / 2;
|
||||||
|
|
||||||
addYAxis();
|
addYAxis();
|
||||||
@@ -244,9 +256,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||||
chartWidth = width - yAxisWidth - margin.right;
|
chartWidth = width - yAxisWidth - margin.right;
|
||||||
|
|
||||||
|
// TODO allow per-y cardWidth!
|
||||||
// we need to fill chartWidth with xBucketSize cards.
|
// we need to fill chartWidth with xBucketSize cards.
|
||||||
xGridSize = chartWidth / (cardsData.xBucketSize+1);
|
xGridSize = chartWidth / (cardsData.xBucketSize+1);
|
||||||
cardWidth = xGridSize - cardSpacing;
|
cardWidth = xGridSize - cardHSpacing;
|
||||||
|
|
||||||
addXAxis();
|
addXAxis();
|
||||||
xAxisHeight = getXAxisHeight(heatmap);
|
xAxisHeight = getXAxisHeight(heatmap);
|
||||||
@@ -299,6 +312,8 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
resetCardHighLight(event);
|
resetCardHighLight(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_renderAnnotations();
|
||||||
|
|
||||||
ctrl.events.emit('render-complete', {
|
ctrl.events.emit('render-complete', {
|
||||||
"chartWidth": chartWidth
|
"chartWidth": chartWidth
|
||||||
});
|
});
|
||||||
@@ -356,7 +371,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
let cx = xScale(d.x);
|
let cx = xScale(d.x);
|
||||||
|
|
||||||
if (cx - cardWidth/2 < 0) {
|
if (cx - cardWidth/2 < 0) {
|
||||||
x = yAxisWidth + cardSpacing/2;
|
x = yAxisWidth + cardHSpacing/2;
|
||||||
} else {
|
} else {
|
||||||
x = yAxisWidth + cx - cardWidth/2;
|
x = yAxisWidth + cx - cardWidth/2;
|
||||||
}
|
}
|
||||||
@@ -372,11 +387,11 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
if (cx < cardWidth/2) {
|
if (cx < cardWidth/2) {
|
||||||
// Center should not exceed half of card.
|
// Center should not exceed half of card.
|
||||||
// Cut card to the left to prevent overlay of y axis.
|
// 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;
|
w = cutted_width > 0 ? cutted_width : 0;
|
||||||
} else if (chartWidth - cx < cardWidth/2) {
|
} else if (chartWidth - cx < cardWidth/2) {
|
||||||
// Cut card to the right to prevent overlay of right graph edge.
|
// 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 {
|
} else {
|
||||||
w = cardWidth;
|
w = cardWidth;
|
||||||
}
|
}
|
||||||
@@ -384,21 +399,25 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Card width should be MIN_CARD_SIZE at least
|
// Card width should be MIN_CARD_SIZE at least
|
||||||
w = Math.max(w, MIN_CARD_SIZE);
|
w = Math.max(w, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardHSpacing == 0) {
|
||||||
|
w = w+1;
|
||||||
|
}
|
||||||
|
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardY(d) {
|
function getCardY(d) {
|
||||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing/2;
|
return yScale(d.y) + chartTop - cardHeight - cardVSpacing/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardHeight(d) {
|
function getCardHeight(d) {
|
||||||
let ys = yScale(d.y);
|
let ys = yScale(d.y);
|
||||||
let y = ys + chartTop - cardHeight - cardSpacing/2;
|
let y = ys + chartTop - cardHeight - cardVSpacing/2;
|
||||||
let h = cardHeight;
|
let h = cardHeight;
|
||||||
|
|
||||||
// Cut card height to prevent overlay
|
// Cut card height to prevent overlay
|
||||||
if (y < chartTop) {
|
if (y < chartTop) {
|
||||||
h = ys - cardSpacing/2;
|
h = ys - cardVSpacing/2;
|
||||||
} else if (ys > chartBottom) {
|
} else if (ys > chartBottom) {
|
||||||
h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
} else if (y + cardHeight > chartBottom) {
|
} 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
|
// Card height should be MIN_CARD_SIZE at least
|
||||||
h = Math.max(h, MIN_CARD_SIZE);
|
h = Math.max(h, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardVSpacing == 0) {
|
||||||
|
h = h+1
|
||||||
|
}
|
||||||
|
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +510,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
|
//annotationTooltip.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(event) {
|
function onMouseMove(event) {
|
||||||
@@ -496,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Clear crosshair and tooltip
|
// Clear crosshair and tooltip
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
tooltip.destroy();
|
tooltip.destroy();
|
||||||
|
annotationTooltip.destroy();
|
||||||
|
|
||||||
selection.x2 = limitSelection(event.offsetX);
|
selection.x2 = limitSelection(event.offsetX);
|
||||||
drawSelection(selection.x1, selection.x2);
|
drawSelection(selection.x1, selection.x2);
|
||||||
@@ -503,6 +528,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
emitGraphHoverEvet(event);
|
emitGraphHoverEvet(event);
|
||||||
drawCrosshair(event.offsetX);
|
drawCrosshair(event.offsetX);
|
||||||
tooltip.show(event); //, data);
|
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("mousedown", onMouseDown);
|
||||||
$heatmap.on("mousemove", onMouseMove);
|
$heatmap.on("mousemove", onMouseMove);
|
||||||
$heatmap.on("mouseleave", onMouseLeave);
|
$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) {
|
function grafanaTimeFormat(ticks, min, max) {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import _ from 'lodash';
|
|||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
|
||||||
|
import './color_legend';
|
||||||
import rendering from './rendering';
|
import rendering from './rendering';
|
||||||
// import aggregates, { aggregatesMap } from './aggregates';
|
// import aggregates, { aggregatesMap } from './aggregates';
|
||||||
// import fragments, { fragmentsMap } from './fragments';
|
// import fragments, { fragmentsMap } from './fragments';
|
||||||
// import { labelFormats } from './xAxisLabelFormats';
|
// import { labelFormats } from './xAxisLabelFormats';
|
||||||
import {statusHeatmapOptionsEditor} from './options_editor';
|
import {statusHeatmapOptionsEditor} from './options_editor';
|
||||||
import {ColorModeDiscrete} from "./color_mode_discrete";
|
import {ColorModeDiscrete} from "./color_mode_discrete";
|
||||||
import './css/status-heatmap.css!';
|
|
||||||
|
|
||||||
const CANVAS = 'CANVAS';
|
const CANVAS = 'CANVAS';
|
||||||
const SVG = 'SVG';
|
const SVG = 'SVG';
|
||||||
@@ -31,7 +31,8 @@ const panelDefaults = {
|
|||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cardMinWidth: 5,
|
cardMinWidth: 5,
|
||||||
cardSpacing: 2,
|
cardVSpacing: 2,
|
||||||
|
cardHSpacing: 2,
|
||||||
cardRound: null
|
cardRound: null
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -57,6 +58,7 @@ const panelDefaults = {
|
|||||||
},
|
},
|
||||||
// how null points should be handled
|
// how null points should be handled
|
||||||
nullPointMode: 'as empty',
|
nullPointMode: 'as empty',
|
||||||
|
yAxisSort: 'metrics',
|
||||||
highlightCards: true,
|
highlightCards: true,
|
||||||
useMax: true
|
useMax: true
|
||||||
};
|
};
|
||||||
@@ -89,7 +91,7 @@ const colorSchemes = [
|
|||||||
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
|
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
|
||||||
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
|
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
|
||||||
{ name: 'YlOrBr', value: 'interpolateYlOrBr', 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'];
|
let colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||||
@@ -98,7 +100,8 @@ let opacityScales = ['linear', 'sqrt'];
|
|||||||
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||||
static templateUrl = 'module.html';
|
static templateUrl = 'module.html';
|
||||||
|
|
||||||
constructor($scope, $injector, $rootScope, timeSrv) {
|
/** @ngInject */
|
||||||
|
constructor($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
_.defaultsDeep(this.panel, panelDefaults);
|
_.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-received', this.onDataReceived);
|
||||||
this.events.on('data-snapshot-load', this.onDataReceived);
|
this.events.on('data-snapshot-load', this.onDataReceived);
|
||||||
this.events.on('data-error', this.onDataError);
|
this.events.on('data-error', this.onDataError);
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode);
|
this.events.on('init-edit-mode', this.onInitEditMode);
|
||||||
this.events.on('render', this.onRender);
|
this.events.on('render', this.onRender);
|
||||||
this.events.on('refresh', this.postRefresh);
|
this.events.on('refresh', this.postRefresh);
|
||||||
|
// custom event from rendering.js
|
||||||
this.events.on('render-complete', this.onRenderComplete);
|
this.events.on('render-complete', this.onRenderComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenderComplete = (data) => {
|
onRenderComplete = (data) => {
|
||||||
this.graph.chartWidth = data.chartWidth;
|
this.graph.chartWidth = data.chartWidth;
|
||||||
|
this.renderingCompleted();
|
||||||
};
|
};
|
||||||
|
|
||||||
// override calculateInterval for discrete color mode
|
// override calculateInterval for discrete color mode
|
||||||
@@ -150,7 +158,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let minCardWidth = this.panel.cards.cardMinWidth;
|
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 maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
|
||||||
|
|
||||||
let intervalMs;
|
let intervalMs;
|
||||||
@@ -185,11 +193,53 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.interval = kbn.secondsToHms(intervalMs / 1000);
|
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) => {
|
onDataReceived = (dataList) => {
|
||||||
this.data = dataList;
|
this.data = dataList;
|
||||||
this.cardsData = this.convertToCards(this.data);
|
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 = () => {
|
onInitEditMode = () => {
|
||||||
@@ -223,6 +273,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
onDataError = () => {
|
onDataError = () => {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
|
this.annotations = [];
|
||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,12 +291,30 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onEditorRemoveThresholds = () => {
|
||||||
|
this.panel.color.thresholds = [];
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
onEditorAddThreeLights = () => {
|
onEditorAddThreeLights = () => {
|
||||||
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
|
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: "yellow", value: 1, tooltip: "warning" });
|
||||||
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
|
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
|
||||||
this.render();
|
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) => {
|
link = (scope, elem, attrs, ctrl) => {
|
||||||
rendering(scope, elem, attrs, ctrl);
|
rendering(scope, elem, attrs, ctrl);
|
||||||
@@ -261,28 +330,27 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
minValue: 0,
|
minValue: 0,
|
||||||
multipleValues: false,
|
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;}
|
if (!data || data.length == 0) { return cardsData;}
|
||||||
|
|
||||||
// collect uniq targets and their indexes in data array
|
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||||
cardsData.targetIndex = {};
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
// collect uniq targets and their indices
|
||||||
let ts = data[i];
|
_.map(data, (d, i) => {
|
||||||
let target = ts.target;
|
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
|
||||||
if (cardsData.targetIndex[target] == undefined) {
|
});
|
||||||
cardsData.targetIndex[target] = []
|
|
||||||
}
|
|
||||||
cardsData.targetIndex[target].push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add some logic for targets heirarchy
|
// TODO add some logic for targets heirarchy
|
||||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||||
cardsData.targets.sort();
|
|
||||||
cardsData.yBucketSize = cardsData.targets.length;
|
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.
|
// 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++) {
|
for(let i = 0; i < cardsData.targets.length; i++) {
|
||||||
let target = cardsData.targets[i];
|
let target = cardsData.targets[i];
|
||||||
|
|
||||||
@@ -292,15 +360,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
values: [],
|
values: [],
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false,
|
noColorDefined: false,
|
||||||
|
y: target,
|
||||||
|
x: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// collect values from all timeseries with target
|
// collect values from all timeseries with target
|
||||||
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||||
let s = data[cardsData.targetIndex[target][si]];
|
let s = data[cardsData.targetIndex[target][si]];
|
||||||
|
if (s.datapoints.length <= j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let datapoint = s.datapoints[j];
|
let datapoint = s.datapoints[j];
|
||||||
if (card.values.length === 0) {
|
if (card.values.length === 0) {
|
||||||
card.x = datapoint[TIME_INDEX];
|
card.x = datapoint[TIME_INDEX];
|
||||||
card.y = s.target;
|
|
||||||
}
|
}
|
||||||
card.values.push(datapoint[VALUE_INDEX]);
|
card.values.push(datapoint[VALUE_INDEX]);
|
||||||
}
|
}
|
||||||
@@ -320,7 +392,9 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
if (cardsData.minValue > card.minValue)
|
if (cardsData.minValue > card.minValue)
|
||||||
cardsData.minValue = card.minValue;
|
cardsData.minValue = card.minValue;
|
||||||
|
|
||||||
|
if (card.x != -1) {
|
||||||
cardsData.cards.push(card);
|
cardsData.cards.push(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class StatusHeatmapTooltip {
|
|||||||
add() {
|
add() {
|
||||||
this.tooltip = d3.select("body")
|
this.tooltip = d3.select("body")
|
||||||
.append("div")
|
.append("div")
|
||||||
.attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip");
|
.attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -79,7 +79,7 @@ export class StatusHeatmapTooltip {
|
|||||||
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
||||||
|
|
||||||
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
|
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') {
|
if (this.panel.color.mode === 'discrete') {
|
||||||
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
||||||
@@ -94,7 +94,7 @@ export class StatusHeatmapTooltip {
|
|||||||
name: <b>${y}</b> <br>
|
name: <b>${y}</b> <br>
|
||||||
${statusesHtml}
|
${statusesHtml}
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||