diff --git a/src/css/worldmap-panel.css b/src/css/worldmap-panel.css index 94cab8b..70ebaf5 100644 --- a/src/css/worldmap-panel.css +++ b/src/css/worldmap-panel.css @@ -6,8 +6,8 @@ padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; - background: rgba(255,255,255,0.8); - box-shadow: 0 0 15px rgba(0,0,0,0.2); + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); border-radius: 5px; } .info h4 { @@ -33,10 +33,58 @@ opacity: 0.7; } -.leaflet-top.leaflet-left, .leaflet-bottom.leaflet-left, .leaflet-bottom.leaflet-right { +.leaflet-top.leaflet-left, +.leaflet-bottom.leaflet-left, +.leaflet-bottom.leaflet-right { z-index: 1000; } -.leaflet-popup-content-wrapper, .leaflet-popup-tip { +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { color: #d8d9da !important; } + +.margin-top-8 { + margin-top: 8px; +} +.tooltip-container { + max-height: 100px; + overflow: auto; + color: #666666; +} + +.info.aggregations.leaflet-control { + color: #666666; + font-size: 11px; + width: 100%; +} +.agg-container { + max-height: 90px; + overflow: auto; +} +.agg-item { + display: flex; + justify-content: space-between; + align-items: center; +} +.agg-item > div:first-child { + min-width: 70px; + display: flex; +} +.agg-label { + width: 40px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 2px; +} +.agg-bar { + flex: 1; + margin-left: 5px; + margin-bottom: 2px; +} + +.agg-bar-progress { + background: #0078a8; + font-size: 0; +} diff --git a/src/css/worldmap.light.css b/src/css/worldmap.light.css index 2ead013..a97b73d 100644 --- a/src/css/worldmap.light.css +++ b/src/css/worldmap.light.css @@ -1,4 +1,3 @@ .worldmap-popup .leaflet-popup-content-wrapper, .worldmap-popup .leaflet-popup-tip { background-color: #ECECEC; - color: #000 !important; } diff --git a/src/data_formatter.ts b/src/data_formatter.ts index 13121f0..065226d 100644 --- a/src/data_formatter.ts +++ b/src/data_formatter.ts @@ -22,7 +22,12 @@ export default class DataFormatter { } if (_.isString(lastValue)) { - data.push({ key: serie.alias, value: 0, valueFormatted: lastValue, valueRounded: 0 }); + data.push({ + key: serie.alias, + value: 0, + valueFormatted: lastValue, + valueRounded: 0 + }); } else { const dataValue = { key: serie.alias, @@ -31,7 +36,7 @@ export default class DataFormatter { locationLongitude: location.longitude, value: serie.stats[this.ctrl.panel.valueName], valueFormatted: lastValue, - valueRounded: 0, + valueRounded: 0 }; if (dataValue.value > highestValue) { @@ -42,7 +47,10 @@ export default class DataFormatter { lowestValue = dataValue.value; } - dataValue.valueRounded = kbn.roundValue(dataValue.value, parseInt(this.ctrl.panel.decimals, 10) || 0); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + parseInt(this.ctrl.panel.decimals, 10) || 0 + ); data.push(dataValue); } }); @@ -61,10 +69,13 @@ export default class DataFormatter { locationLongitude: decodedGeohash.longitude, value: value, valueFormatted: value, - valueRounded: 0, + valueRounded: 0 }; - dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + this.ctrl.panel.decimals || 0 + ); return dataValue; } @@ -93,7 +104,12 @@ export default class DataFormatter { : encodedGeohash; const value = row[columnNames[this.ctrl.panel.esMetric]]; - const dataValue = this.createDataValue(encodedGeohash, decodedGeohash, locationName, value); + const dataValue = this.createDataValue( + encodedGeohash, + decodedGeohash, + locationName, + value + ); if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -117,7 +133,12 @@ export default class DataFormatter { : encodedGeohash; const value = datapoint[this.ctrl.panel.esMetric]; - const dataValue = this.createDataValue(encodedGeohash, decodedGeohash, locationName, value); + const dataValue = this.createDataValue( + encodedGeohash, + decodedGeohash, + locationName, + value + ); if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -169,9 +190,12 @@ export default class DataFormatter { let key; let longitude; let latitude; + let metricFieldChoosen: number; + let metricFieldNaN; if (this.ctrl.panel.tableQueryOptions.queryType === 'geohash') { - const encodedGeohash = datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; + const encodedGeohash = + datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; const decodedGeohash = decodeGeoHash(encodedGeohash); latitude = decodedGeohash.latitude; @@ -179,20 +203,40 @@ export default class DataFormatter { key = encodedGeohash; } else { latitude = datapoint[this.ctrl.panel.tableQueryOptions.latitudeField]; - longitude = datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; + longitude = + datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; key = `${latitude}_${longitude}`; } - + let locationName; + if (this.ctrl.panel.tableQueryOptions.metricField === 'TAS') { + locationName = (datapoint[this.ctrl.panel.tableQueryOptions.labelField] || datapoint["Name"]); + if (datapoint["State"] === "ACTIVE"){ + metricFieldChoosen = 11; + }else{ + metricFieldChoosen = -1; + } + metricFieldNaN = true; + } else { + metricFieldChoosen = + datapoint[this.ctrl.panel.tableQueryOptions.metricField]; + metricFieldNaN = false; + locationName = (datapoint[this.ctrl.panel.tableQueryOptions.labelField] || "n/a"); + } const dataValue = { key: key, - locationName: datapoint[this.ctrl.panel.tableQueryOptions.labelField] || 'n/a', + locationName: locationName, locationLatitude: latitude, locationLongitude: longitude, - value: datapoint[this.ctrl.panel.tableQueryOptions.metricField], - valueFormatted: datapoint[this.ctrl.panel.tableQueryOptions.metricField], + value: metricFieldChoosen || 0, + valueFormatted: + datapoint[this.ctrl.panel.tableQueryOptions.metricField], valueRounded: 0, + isMetricFieldNaN: metricFieldNaN }; - + if (this.ctrl.panel.aggregationLegendField !== '') { + dataValue[`agg-${this.ctrl.panel.aggregationLegendField}`] = + datapoint[this.ctrl.panel.aggregationLegendField]; + } if (dataValue.value > highestValue) { highestValue = dataValue.value; } @@ -201,10 +245,38 @@ export default class DataFormatter { lowestValue = dataValue.value; } - dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); - data.push(dataValue); + dataValue.valueRounded = kbn.roundValue( + dataValue.value, + this.ctrl.panel.decimals || 0 + ); + if (latitude && longitude) { + data.push(dataValue); + } }); - + // Getting the List of columns from the table row + data.columns = Object.keys(tableData[0][0] || []); + // Aggregations + if ( + (this.ctrl.panel.aggregationLegendField !== '' && + data.columns.indexOf(this.ctrl.panel.aggregationLegendField) > -1) || + !tableData[0][0] + ) { + data.aggregations = _.countBy( + data, + `agg-${this.ctrl.panel.aggregationLegendField}` + ); + data.aggregations.unknown = + data.aggregations.Undefined || 0 + data.aggregations[''] || 0; + delete data.aggregations.undefined; + delete data.aggregations['']; + data.aggregationSortedList = Object.keys(data.aggregations).sort( + function(a, b) { + return data.aggregations[b] - data.aggregations[a]; + } + ); + } else { + data.aggregations = {}; + } data.highestValue = highestValue; data.lowestValue = lowestValue; data.valueRange = highestValue - lowestValue; @@ -223,7 +295,7 @@ export default class DataFormatter { locationLatitude: point.latitude, locationLongitude: point.longitude, value: point.value !== undefined ? point.value : 1, - valueRounded: 0, + valueRounded: 0 }; if (dataValue.value > highestValue) { highestValue = dataValue.value; diff --git a/src/partials/editor.html b/src/partials/editor.html index 63c474e..6720596 100644 --- a/src/partials/editor.html +++ b/src/partials/editor.html @@ -1,50 +1,119 @@ -
+ + +
Map Visual Options
- +
- - + +
- +
- +
- +
- +
- +
- - + +
- - + +
Map Data Options
@@ -52,161 +121,379 @@
Map Data Options
- +
- +
-
+
-
-
+
-
-
+
- +
-
+
Mapping Between Time Series Query and Worldmap
-

The query should be formatted as Time Series data. The time series (group by) name should be a 2-letter country code. - For example: US for United States or FR for France.

+

+ The query should be formatted as Time Series data. The time series + (group by) name should be a 2-letter country code. For example: US for + United States or FR for France. +

-
+
Mapping Between Time Series Query and Worldmap
-

The query should be formatted as Time Series data. The time series (group by) name should be a 3-letter country code. - For example: USA for United States or FRA for France.

+

+ The query should be formatted as Time Series data. The time series + (group by) name should be a 3-letter country code. For example: USA for + United States or FRA for France. +

-
-
Mapping Between Time Series Query and Worldmap
-

The query should be formatted as Time Series data. The time series (group by) name should be a 2-letter US state code. - For example: CA for California.

-
-
+
+
Mapping Between Time Series Query and Worldmap
+

+ The query should be formatted as Time Series data. The time series + (group by) name should be a 2-letter US state code. For example: CA for + California. +

+
+
Mapping Between Geohash Query and Worldmap
-

The query should be an Elasticsearch using the Geo Hash Grid feature or a Prometheus query that returns geohashes.

+

+ The query should be an Elasticsearch using the Geo Hash Grid feature or + a Prometheus query that returns geohashes. +

  • - Location Name Field (optional): enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the geohash value is used as the label.
  • + Location Name Field (optional): enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the geohash value is used as the label. +
  • - geo_point/geohash Field: enter the name of the geo_point/geohash column. This is used to calculate where the - circle should be drawn.
  • + geo_point/geohash Field: enter the name of the + geo_point/geohash column. This is used to calculate where the circle + should be drawn. +
  • - Metric Field: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.
  • + Metric Field: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. +
-
+
Mapping Between Table Query and Worldmap
-

The query should be formatted as Table data and have a geohash column and a numeric metric column.

+

+ The query should be formatted as Table data and have a geohash column + and a numeric metric column. +

  • - Location Name Field (optional): enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the geohash value is used as the label.
  • + Location Name Field (optional): enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the geohash value is used as the label. +
  • - Geohash Field: enter the name of the geohash column. This is used to calculate where the circle should be drawn.
  • + Geohash Field: enter the name of the geohash column. This is + used to calculate where the circle should be drawn. +
  • - Metric Field: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.
  • + Metric Field: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. +
-
+
Mapping Between Table Query and Worldmap
-

The query should be formatted as Table data and contain latitude, longitude columns and a numeric metric column.

+

+ The query should be formatted as Table data and contain latitude, + longitude columns and a numeric metric column. +

  • - Location Name Field (optional): enter the name of the Location Name column. Used to label each circle on the - map. If it is empty then the value N/A is used as the label.
  • + Location Name Field (optional): enter the name of the Location + Name column. Used to label each circle on the map. If it is empty then + the value N/A is used as the label. +
  • - Latitude/Longitude Fields: enter the name of the latitude and longitude columns. These are used to calculate - where the circle should be drawn.
  • + Latitude/Longitude Fields: enter the name of the latitude and + longitude columns. These are used to calculate where the circle should + be drawn. +
  • - Metric Field: enter the name of the metric column. This is used to give the circle a value - this determines - how large the circle is.
  • + Metric Field: enter the name of the metric column. This is used + to give the circle a value - this determines how large the circle is. + If the selected Datasource is AIOps_Inventory, then specify the value as TAS. +
-
Field Mapping
+
+ Field Mapping +
- +
- +
- +
- +
- +
- +
- +
- +
-
+
+
+
+ Aggregations Legends + When enabled, points that are plotted on the world map are aggregated by the selected Aggregation_Field and the aggregated result is displayed on the chart. + +
+ + +
+ + +
+
+
+
+
+
DrillDown
+
+ + +
+ + +
+
Threshold Options
- +
- +
Hide series
- + - +
+ +
+
Regex
+
+ + +
+
diff --git a/src/plugin.json b/src/plugin.json index 1afa70a..8f590d2 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -23,8 +23,8 @@ {"name": "USA", "path": "images/worldmap-usa.png"}, {"name": "Light Theme", "path": "images/worldmap-light-theme.png"} ], - "version": "1.0.1", - "updated": "Fri May 15 14:40:24 MDT 2020" + "version": "0.2.1", + "updated": "2019-08-29" }, "dependencies": { diff --git a/src/worldmap.ts b/src/worldmap.ts index 341f9a6..0e52a93 100644 --- a/src/worldmap.ts +++ b/src/worldmap.ts @@ -4,19 +4,21 @@ import WorldmapCtrl from './worldmap_ctrl'; const tileServers = { 'CartoDB Positron': { - url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', + url: + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', attribution: '© OpenStreetMap ' + '© CartoDB', - subdomains: 'abcd', + subdomains: 'abcd' }, 'CartoDB Dark': { - url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', + url: + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', attribution: '© OpenStreetMap ' + '© CartoDB', - subdomains: 'abcd', - }, + subdomains: 'abcd' + } }; export default class WorldMap { @@ -26,11 +28,14 @@ export default class WorldMap { map: any; legend: any; circlesLayer: any; + aggregations: any; + templateSrv: any; - constructor(ctrl, mapContainer) { + constructor(ctrl, mapContainer, templateSrv?) { this.ctrl = ctrl; this.mapContainer = mapContainer; this.circles = []; + this.templateSrv = templateSrv; } createMap() { @@ -42,7 +47,7 @@ export default class WorldMap { worldCopyJump: true, preferCanvas: true, center: mapCenter, - zoom: parseInt(this.ctrl.panel.initialZoom, 10) || 1, + zoom: parseInt(this.ctrl.panel.initialZoom, 10) || 1 }); this.setMouseWheelZoom(); @@ -52,7 +57,7 @@ export default class WorldMap { subdomains: selectedTileServer.subdomains, reuseTiles: true, detectRetina: true, - attribution: selectedTileServer.attribution, + attribution: selectedTileServer.attribution }).addTo(this.map); } @@ -80,13 +85,63 @@ export default class WorldMap { this.ctrl.panel.colors[index + 1] + '"> ' + thresholds[index] + - (thresholds[index + 1] ? '–' + thresholds[index + 1] + '
' : '+'); + (thresholds[index + 1] + ? '–' + thresholds[index + 1] + '
' + : '+'); } this.legend._div.innerHTML = legendHtml; }; this.legend.addTo(this.map); } + createAggregations() { + this.aggregations = (window).L.control({ position: 'bottomright' }); + this.aggregations.onAdd = () => { + this.aggregations._div = (window).L.DomUtil.create( + 'div', + 'info aggregations' + ); + this.aggregations.update(); + return this.aggregations._div; + }; + + this.aggregations.update = () => { + const aggregations = this.ctrl.data.aggregations || {}; + let aggregationsHtml = ''; + if (Object.keys(aggregations).length <= 1 && aggregations.unknown === 0) { + aggregationsHtml = '
No Data Available
'; + } else if ( + this.ctrl.data.aggregationSortedList && + this.ctrl.data.aggregationSortedList.length && + Object.keys(aggregations).length > 1 + ) { + this.ctrl.data.aggregationSortedList.forEach(agg => { + if (aggregations[agg] !== 0) { + aggregationsHtml = `${aggregationsHtml} +
+
+
${agg}
+ ${(100 * (aggregations[agg] / this.ctrl.data.length)) + .toFixed(2) + .replace(/[.,]00$/, '')}% +
+
+
+ progress +
+
+
`; + } + }); + } else { + aggregationsHtml = `Invalid mapping for aggregations`; + } + this.aggregations._div.innerHTML = `
${aggregationsHtml}
`; + }; + this.aggregations.addTo(this.map); + } + needToRedrawCircles(data) { if (this.circles.length === 0 && data.length > 0) { return true; @@ -103,7 +158,10 @@ export default class WorldMap { filterEmptyAndZeroValues(data) { return _.filter(data, o => { - return !(this.ctrl.panel.hideEmpty && _.isNil(o.value)) && !(this.ctrl.panel.hideZero && o.value === 0); + return ( + !(this.ctrl.panel.hideEmpty && _.isNil(o.value)) && + !(this.ctrl.panel.hideZero && o.value === 0) + ); }); } @@ -117,11 +175,17 @@ export default class WorldMap { drawCircles() { const data = this.filterEmptyAndZeroValues(this.ctrl.data); - if (this.needToRedrawCircles(data)) { + this.clearCircles(); + this.createCircles(data); + /*if (this.needToRedrawCircles(data)) { this.clearCircles(); this.createCircles(data); } else { this.updateCircles(data); + }*/ + // Update Aggregations + if (this.aggregations && this.aggregations !== null) { + this.aggregations.update(); } } @@ -131,7 +195,7 @@ export default class WorldMap { if (!dataPoint.locationName) { return; } - circles.push(this.createCircle(dataPoint)); + circles.push(this.createCircle(dataPoint, data)); }); this.circlesLayer = this.addCircles(circles); this.circles = circles; @@ -148,66 +212,120 @@ export default class WorldMap { }); if (circle) { + circle.options.dataset = data; circle.setRadius(this.calcCircleSize(dataPoint.value || 0)); circle.setStyle({ color: this.getColor(dataPoint.value), fillColor: this.getColor(dataPoint.value), fillOpacity: 0.5, - location: dataPoint.key, + location: dataPoint.key }); circle.unbindPopup(); - this.createPopup(circle, dataPoint.locationName, dataPoint.valueRounded); + this.createPopup( + circle, + dataPoint.locationName, + dataPoint.valueRounded, + dataPoint.isMetricFieldNaN + ); } }); + if (this.aggregations && this.aggregations !== null) { + this.aggregations.update(); + } } - createCircle(dataPoint) { - const circle = (window).L.circleMarker([dataPoint.locationLatitude, dataPoint.locationLongitude], { - radius: this.calcCircleSize(dataPoint.value || 0), - color: this.getColor(dataPoint.value), - fillColor: this.getColor(dataPoint.value), - fillOpacity: 0.5, - location: dataPoint.key, - }); - - this.createPopup(circle, dataPoint.locationName, dataPoint.valueRounded); + createCircle(dataPoint, dataset?) { + const circle = (window).L.circleMarker( + [dataPoint.locationLatitude, dataPoint.locationLongitude], + { + radius: this.calcCircleSize(dataPoint.value || 0), + color: this.getColor(dataPoint.value), + fillColor: this.getColor(dataPoint.value), + fillOpacity: 0.5, + location: dataPoint.key, + dataset + } + ); + this.createPopup( + circle, + dataPoint.locationName, + dataPoint.valueRounded, + dataPoint.isMetricFieldNaN + ); return circle; } calcCircleSize(dataPointValue) { - const circleMinSize = parseInt(this.ctrl.panel.circleMinSize, 10) || 2; - const circleMaxSize = parseInt(this.ctrl.panel.circleMaxSize, 10) || 30; + const circleMinSize = parseInt(this.ctrl.panel.circleMinSize, 10) || 10; + const circleMaxSize = parseInt(this.ctrl.panel.circleMaxSize, 10) || 10; if (this.ctrl.data.valueRange === 0) { return circleMaxSize; } - const dataFactor = (dataPointValue - this.ctrl.data.lowestValue) / this.ctrl.data.valueRange; + const dataFactor = + (dataPointValue - this.ctrl.data.lowestValue) / this.ctrl.data.valueRange; const circleSizeRange = circleMaxSize - circleMinSize; return circleSizeRange * dataFactor + circleMinSize; } - createPopup(circle, locationName, value) { - const unit = value && value === 1 ? this.ctrl.panel.unitSingular : this.ctrl.panel.unitPlural; - const label = (locationName + ': ' + value + ' ' + (unit || '')).trim(); - circle.bindPopup(label, { - offset: (window).L.point(0, -2), - className: 'worldmap-popup', - closeButton: this.ctrl.panel.stickyLabels, - }); + createPopup(circle, locationName, value, isMetricFieldNaN) { + const unit = + value && value === 1 + ? this.ctrl.panel.unitSingular + : this.ctrl.panel.unitPlural; + + let formattedLocationList = ''; + circle.options.dataset + .filter(data => data.key === circle.options.location) + .forEach(location => { + if ( + this.ctrl.panel.drilldownTarget && + this.ctrl.panel.drilldownTarget !== '' + ) { + const url = this.ctrl.panel.drilldownTarget + ? this.ctrl.panel.drilldownTarget.split('${__locationName}').join(`${location.locationName}`) : ''; + const hrefUrl = this.templateSrv.replace(url); + + formattedLocationList = `${formattedLocationList} +
+ ${` + ${ + this.ctrl.panel.drillDownTab + ? `${location.locationName} ` + : `${location.locationName}` + } + ${!isMetricFieldNaN ? `: ${location.value}` : ''} + ${unit ? `(${unit})` : ''}`.trim()} +
`; + } else { + formattedLocationList = `${formattedLocationList}
+ ${`${location.locationName} ${ + !isMetricFieldNaN ? `: ${location.value}` : '' + } + ${unit ? `(${unit})` : ''}`.trim()} +
`; + } + const label = `
${formattedLocationList}
`; + circle.bindPopup(label, { + offset: (window).L.point(0, -2), + className: 'worldmap-popup', + closeButton: this.ctrl.panel.stickyLabels + }); - circle.on('mouseover', function onMouseOver(evt) { - const layer = evt.target; - layer.bringToFront(); - this.openPopup(); - }); + circle.on('mouseover', function onMouseOver(evt) { + const layer = evt.target; + layer.bringToFront(); + this.openPopup(); + }); - if (!this.ctrl.panel.stickyLabels) { - circle.on('mouseout', function onMouseOut() { - circle.closePopup(); + if (!this.ctrl.panel.stickyLabels) { + circle.on('mouseout', function onMouseOut() { + circle.closePopup(); + }); + } }); - } } getColor(value) { @@ -224,7 +342,10 @@ export default class WorldMap { } panToMapCenter() { - this.map.panTo([parseFloat(this.ctrl.panel.mapCenterLatitude), parseFloat(this.ctrl.panel.mapCenterLongitude)]); + this.map.panTo([ + parseFloat(this.ctrl.panel.mapCenterLatitude), + parseFloat(this.ctrl.panel.mapCenterLongitude) + ]); this.ctrl.mapCenterMoved = false; } @@ -233,6 +354,11 @@ export default class WorldMap { this.legend = null; } + removeAggregations() { + this.aggregations.remove(this.map); + this.aggregations = null; + } + setMouseWheelZoom() { if (!this.ctrl.panel.mouseWheelZoom) { this.map.scrollWheelZoom.disable(); @@ -261,6 +387,9 @@ export default class WorldMap { if (this.legend) { this.removeLegend(); } + if (this.aggregations) { + this.removeAggregations(); + } this.map.remove(); } } diff --git a/src/worldmap_ctrl.ts b/src/worldmap_ctrl.ts index 4aa746c..e5fe43a 100644 --- a/src/worldmap_ctrl.ts +++ b/src/worldmap_ctrl.ts @@ -1,59 +1,61 @@ -import { MetricsPanelCtrl } from "grafana/app/plugins/sdk"; -import TimeSeries from "grafana/app/core/time_series2"; +import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; +import TimeSeries from 'grafana/app/core/time_series2'; import appEvents from 'grafana/app/core/app_events'; -import * as _ from "lodash"; -import DataFormatter from "./data_formatter"; -import "./css/worldmap-panel.css"; -import $ from "jquery"; -import "./css/leaflet.css"; -import WorldMap from "./worldmap"; +import * as _ from 'lodash'; +import DataFormatter from './data_formatter'; +import './css/worldmap-panel.css'; +import $ from 'jquery'; +import './css/leaflet.css'; +import WorldMap from './worldmap'; const panelDefaults = { maxDataPoints: 1, - mapCenter: "(0°, 0°)", + mapCenter: '(0°, 0°)', mapCenterLatitude: 0, mapCenterLongitude: 0, initialZoom: 1, - valueName: "total", - circleMinSize: 2, - circleMaxSize: 30, - locationData: "countries", - thresholds: "0,10", + valueName: 'total', + circleMinSize: 10, + circleMaxSize: 10, + locationData: 'table', + thresholds: '0,10', colors: [ - "rgba(245, 54, 54, 0.9)", - "rgba(237, 129, 40, 0.89)", - "rgba(50, 172, 45, 0.97)" + 'rgba(245, 54, 54, 0.9)', + 'rgba(237, 129, 40, 0.89)', + 'rgba(50, 172, 45, 0.97)' ], - unitSingle: "", - unitPlural: "", + unitSingle: '', + unitPlural: '', showLegend: true, mouseWheelZoom: false, - esMetric: "Count", + esMetric: 'Count', decimals: 0, hideEmpty: false, hideZero: false, stickyLabels: false, tableQueryOptions: { - queryType: "geohash", - geohashField: "geohash", - latitudeField: "latitude", - longitudeField: "longitude", - metricField: "metric" - } + queryType: 'coordinates', + geohashField: 'geohash', + latitudeField: 'latitude', + longitudeField: 'longitude', + metricField: 'metric' + }, + aggregationLegends: false, + aggregationLegendField: '' }; const mapCenters = { - "(0°, 0°)": { mapCenterLatitude: 0, mapCenterLongitude: 0 }, - "North America": { mapCenterLatitude: 40, mapCenterLongitude: -100 }, + '(0°, 0°)': { mapCenterLatitude: 0, mapCenterLongitude: 0 }, + 'North America': { mapCenterLatitude: 40, mapCenterLongitude: -100 }, Europe: { mapCenterLatitude: 46, mapCenterLongitude: 14 }, - "West Asia": { mapCenterLatitude: 26, mapCenterLongitude: 53 }, - "SE Asia": { mapCenterLatitude: 10, mapCenterLongitude: 106 }, - "Last GeoHash": { mapCenterLatitude: 0, mapCenterLongitude: 0 } + 'West Asia': { mapCenterLatitude: 26, mapCenterLongitude: 53 }, + 'SE Asia': { mapCenterLatitude: 10, mapCenterLongitude: 106 }, + 'Last GeoHash': { mapCenterLatitude: 0, mapCenterLongitude: 0 } }; export default class WorldmapCtrl extends MetricsPanelCtrl { - static templateUrl = "partials/module.html"; + static templateUrl = 'partials/module.html'; dataFormatter: DataFormatter; locations: any; @@ -73,26 +75,26 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.dataFormatter = new DataFormatter(this); - this.events.on("init-edit-mode", this.onInitEditMode.bind(this)); - this.events.on("data-received", this.onDataReceived.bind(this)); - this.events.on("panel-teardown", this.onPanelTeardown.bind(this)); - this.events.on("data-snapshot-load", this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('panel-teardown', this.onPanelTeardown.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); this.loadLocationDataFromFile(); } setMapProvider(contextSrv) { this.tileServer = contextSrv.user.lightTheme - ? "CartoDB Positron" - : "CartoDB Dark"; + ? 'CartoDB Positron' + : 'CartoDB Dark'; this.setMapSaturationClass(); } setMapSaturationClass() { - if (this.tileServer === "CartoDB Dark") { - this.saturationClass = "map-darken"; + if (this.tileServer === 'CartoDB Dark') { + this.saturationClass = 'map-darken'; } else { - this.saturationClass = ""; + this.saturationClass = ''; } } @@ -106,23 +108,23 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { return; } - if (this.panel.locationData === "jsonp endpoint") { + if (this.panel.locationData === 'jsonp endpoint') { if (!this.panel.jsonpUrl || !this.panel.jsonpCallback) { return; } $.ajax({ - type: "GET", - url: this.panel.jsonpUrl + "?callback=?", - contentType: "application/json", + type: 'GET', + url: this.panel.jsonpUrl + '?callback=?', + contentType: 'application/json', jsonpCallback: this.panel.jsonpCallback, - dataType: "jsonp", + dataType: 'jsonp', success: res => { this.locations = res; this.render(); } }); - } else if (this.panel.locationData === "json endpoint") { + } else if (this.panel.locationData === 'json endpoint') { if (!this.panel.jsonUrl) { return; } @@ -131,16 +133,16 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.locations = res; this.render(); }); - } else if (this.panel.locationData === "table") { + } else if (this.panel.locationData === 'table') { // .. Do nothing } else if ( - this.panel.locationData !== "geohash" && - this.panel.locationData !== "json result" + this.panel.locationData !== 'geohash' && + this.panel.locationData !== 'json result' ) { $.getJSON( - "public/plugins/grafana-worldmap-panel/data/" + + 'public/plugins/grafana-worldmap-panel/data/' + this.panel.locationData + - ".json" + '.json' ).then(this.reloadLocations.bind(this)); } } @@ -152,15 +154,15 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { showTableGeohashOptions() { return ( - this.panel.locationData === "table" && - this.panel.tableQueryOptions.queryType === "geohash" + this.panel.locationData === 'table' && + this.panel.tableQueryOptions.queryType === 'geohash' ); } showTableCoordinateOptions() { return ( - this.panel.locationData === "table" && - this.panel.tableQueryOptions.queryType === "coordinates" + this.panel.locationData === 'table' && + this.panel.tableQueryOptions.queryType === 'coordinates' ); } @@ -171,9 +173,9 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab( - "Worldmap", - "public/plugins/grafana-worldmap-panel/partials/editor.html", + this.addEditorTab( + 'Worldmap', + 'public/plugins/grafana-worldmap-panel/partials/editor.html', 2 ); } @@ -190,12 +192,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { const data = []; - if (this.panel.locationData === "geohash") { + if (this.panel.locationData === 'geohash') { this.dataFormatter.setGeohashValues(dataList, data); - } else if (this.panel.locationData === "table") { + } else if (this.panel.locationData === 'table') { const tableData = dataList.map(DataFormatter.tableHandler.bind(this)); this.dataFormatter.setTableValues(tableData, data); - } else if (this.panel.locationData === "json result") { + } else if (this.panel.locationData === 'json result') { this.series = dataList; this.dataFormatter.setJsonValues(data); } else { @@ -206,13 +208,13 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.updateThresholdData(); - if (this.data.length && this.panel.mapCenter === "Last GeoHash") { + if (this.data.length && this.panel.mapCenter === 'Last GeoHash') { this.centerOnLastGeoHash(); } else { this.render(); } } catch (err) { - appEvents.emit('alert-error', ['Data error', err.toString()]) + appEvents.emit('alert-error', ['Data error', err.toString()]); } } @@ -239,7 +241,7 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } setNewMapCenter() { - if (this.panel.mapCenter !== "custom") { + if (this.panel.mapCenter !== 'custom') { this.panel.mapCenterLatitude = mapCenters[this.panel.mapCenter].mapCenterLatitude; this.panel.mapCenterLongitude = @@ -260,6 +262,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { this.render(); } + toggleAggregations() { + if (!this.panel.aggregationLegends) { + this.map.removeAggregations(); + } + this.render(); + } toggleMouseWheelZoom() { this.map.setMouseWheelZoom(); this.render(); @@ -277,7 +285,7 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } updateThresholdData() { - this.data.thresholds = this.panel.thresholds.split(",").map(strValue => { + this.data.thresholds = this.panel.thresholds.split(',').map(strValue => { return Number(strValue.trim()); }); while (_.size(this.panel.colors) > _.size(this.data.thresholds) + 1) { @@ -286,15 +294,69 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } while (_.size(this.panel.colors) < _.size(this.data.thresholds) + 1) { // not enough colors. add one. - const newColor = "rgba(50, 172, 45, 0.97)"; + const newColor = 'rgba(50, 172, 45, 0.97)'; this.panel.colors.push(newColor); } } + stringStartsAsRegEx(str: string): boolean { + if (!str) { + return false; + } + + return str[0] === '/'; + } + + stringToJsRegex(str: string): RegExp { + if (!this.stringStartsAsRegEx(str)) { + return new RegExp(`^${str}$`); + } + + const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + + if (!match) { + throw new Error(`'${str}' is not a valid regular expression.`); + } + + return new RegExp(match[1], match[2]); + } + applyRegexPattern() { + let seriesList = this.data; + for (let i = 0; i < seriesList.length; i++) { + if (!seriesList[i].locName) { + seriesList[i].locName = seriesList[i].locationName + } + if (this.panel.regexPattern !== '' && this.panel.regexPattern !== undefined) { + const regexVal = this.stringToJsRegex(this.panel.regexPattern); + if (seriesList[i].locName && regexVal.test(seriesList[i].locName.toString())) { + const temp = regexVal.exec(seriesList[i].locName.toString()); + if (!temp) { + continue; + } + let extractedtxt = ''; + if (temp.length > 1) { + temp.slice(1).forEach((value, i) => { + if (value) { + extractedtxt += extractedtxt.length > 0 ? ' ' + value.toString() : value.toString(); + } + }); + seriesList[i].locationName = extractedtxt; + } + } + else { + seriesList[i].locationName = seriesList[i].locName; + } + } + else { + seriesList[i].locationName = seriesList[i].locName; + } + } + this.data = seriesList; + } changeLocationData() { this.loadLocationDataFromFile(true); - if (this.panel.locationData === "geohash") { + if (this.panel.locationData === 'geohash') { this.render(); } } @@ -302,12 +364,12 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { link(scope, elem, attrs, ctrl) { let firstRender = true; - ctrl.events.on("render", () => { + ctrl.events.on('render', () => { render(); ctrl.renderingCompleted(); }); - function render() { + function render() { if (!ctrl.data) { return; } @@ -319,14 +381,16 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { return; } - const mapContainer = elem.find(".mapcontainer"); + ctrl.applyRegexPattern(); + + const mapContainer = elem.find('.mapcontainer'); - if (mapContainer[0].id.indexOf("{{") > -1) { + if (mapContainer[0].id.indexOf('{{') > -1) { return; } if (!ctrl.map) { - const map = new WorldMap(ctrl, mapContainer[0]); + const map = new WorldMap(ctrl, mapContainer[0], ctrl.templateSrv); map.createMap(); ctrl.map = map; } @@ -341,6 +405,10 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { ctrl.map.createLegend(); } + if (!ctrl.map.aggregations && ctrl.panel.aggregationLegends) { + ctrl.map.createAggregations(); + } + ctrl.map.drawCircles(); } }