From 68f5e75eba8a3bd57b254740f61db743afa18b98 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 24 Mar 2017 13:35:19 +0300 Subject: [PATCH 01/37] Move heatmap panel into core grafana. --- public/app/headers/common.d.ts | 5 + public/app/plugins/panel/heatmap/README.md | 0 .../app/plugins/panel/heatmap/axes_editor.ts | 43 + .../panel/heatmap/css/heatmap.dark.css | 70 ++ .../panel/heatmap/css/heatmap.light.css | 58 ++ .../plugins/panel/heatmap/display_editor.ts | 26 + .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 220 +++++ .../panel/heatmap/heatmap_data_converter.ts | 357 ++++++++ .../plugins/panel/heatmap/heatmap_tooltip.ts | 248 +++++ .../panel/heatmap/img/icn-heatmap-panel.svg | 195 ++++ public/app/plugins/panel/heatmap/module.html | 7 + public/app/plugins/panel/heatmap/module.ts | 13 + .../panel/heatmap/partials/axes_editor.html | 85 ++ .../heatmap/partials/display_editor.html | 93 ++ public/app/plugins/panel/heatmap/plugin.json | 16 + public/app/plugins/panel/heatmap/rendering.ts | 861 ++++++++++++++++++ public/app/system.conf.js | 3 +- public/vendor/d3/LICENSE | 27 + public/vendor/d3/README.md | 57 ++ public/vendor/d3/d3-scale-chromatic.min.js | 2 + public/vendor/d3/d3.js | 3 + public/vendor/d3/d3.v4.min.js | 8 + 22 files changed, 2396 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/heatmap/README.md create mode 100644 public/app/plugins/panel/heatmap/axes_editor.ts create mode 100644 public/app/plugins/panel/heatmap/css/heatmap.dark.css create mode 100644 public/app/plugins/panel/heatmap/css/heatmap.light.css create mode 100644 public/app/plugins/panel/heatmap/display_editor.ts create mode 100644 public/app/plugins/panel/heatmap/heatmap_ctrl.ts create mode 100644 public/app/plugins/panel/heatmap/heatmap_data_converter.ts create mode 100644 public/app/plugins/panel/heatmap/heatmap_tooltip.ts create mode 100644 public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg create mode 100644 public/app/plugins/panel/heatmap/module.html create mode 100644 public/app/plugins/panel/heatmap/module.ts create mode 100644 public/app/plugins/panel/heatmap/partials/axes_editor.html create mode 100644 public/app/plugins/panel/heatmap/partials/display_editor.html create mode 100644 public/app/plugins/panel/heatmap/plugin.json create mode 100644 public/app/plugins/panel/heatmap/rendering.ts create mode 100644 public/vendor/d3/LICENSE create mode 100644 public/vendor/d3/README.md create mode 100644 public/vendor/d3/d3-scale-chromatic.min.js create mode 100644 public/vendor/d3/d3.js create mode 100644 public/vendor/d3/d3.v4.min.js diff --git a/public/app/headers/common.d.ts b/public/app/headers/common.d.ts index aa4825829a1..9ea5e96654d 100644 --- a/public/app/headers/common.d.ts +++ b/public/app/headers/common.d.ts @@ -67,3 +67,8 @@ declare module 'remarkable' { var config: any; export default config; } + +declare module 'd3' { + var d3: any; + export default d3; +} diff --git a/public/app/plugins/panel/heatmap/README.md b/public/app/plugins/panel/heatmap/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/plugins/panel/heatmap/axes_editor.ts b/public/app/plugins/panel/heatmap/axes_editor.ts new file mode 100644 index 00000000000..662b1aba406 --- /dev/null +++ b/public/app/plugins/panel/heatmap/axes_editor.ts @@ -0,0 +1,43 @@ +/// + +import kbn from 'app/core/utils/kbn'; + +export class AxesEditorCtrl { + panel: any; + panelCtrl: any; + unitFormats: any; + logScales: any; + + /** @ngInject */ + constructor($scope) { + $scope.editor = this; + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + + this.unitFormats = kbn.getUnitFormats(); + + this.logScales = { + 'linear': 1, + 'log (base 2)': 2, + 'log (base 10)': 10, + 'log (base 32)': 32, + 'log (base 1024)': 1024 + }; + } + + setUnitFormat(subItem) { + this.panel.yAxis.format = subItem.value; + this.panelCtrl.render(); + } +} + +/** @ngInject */ +export function axesEditor() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/app/plugins/panel/heatmap/partials/axes_editor.html', + controller: AxesEditorCtrl, + }; +} diff --git a/public/app/plugins/panel/heatmap/css/heatmap.dark.css b/public/app/plugins/panel/heatmap/css/heatmap.dark.css new file mode 100644 index 00000000000..1a2ba8b7eb0 --- /dev/null +++ b/public/app/plugins/panel/heatmap/css/heatmap.dark.css @@ -0,0 +1,70 @@ +.axis text { +} + +.axis { + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: smaller; + fill: #D8D9DA; +} + +.axis path, +.axis line { + fill: none; + stroke: #7B7B7B; + /*shape-rendering: crispEdges;*/ +} + +.axis .domain { + /*opacity: 0;*/ +} + +.tick line { + opacity: 0.4; + stroke: #7B7B7B; +} + +.tick text { + fill: #D8D9DA; +} + +.heatmap-panel { + cursor: crosshair; + padding: 0; +} + +div.heatmap-tooltip { + position: absolute; + text-align: left; + min-width: 160px; + padding: 12px; + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: 13px; + background: #141414; + border: 0px; + border-radius: 8px; + pointer-events: none; +} + +.card-highlighted:hover { + stroke: #D8D9DA; +} + +rect.heatmap-card { + pointer-events: all; +} + +.heatmap-histogram rect { + fill: #828282; +} + +.heatmap-crosshair line { + stroke: #9a1010; + stroke-width: 1; +} + +.heatmap-selection { + stroke-width: 1; + opacity: 0.3; + fill: #828282; + stroke: #D8D9DA; +} diff --git a/public/app/plugins/panel/heatmap/css/heatmap.light.css b/public/app/plugins/panel/heatmap/css/heatmap.light.css new file mode 100644 index 00000000000..bf9504fc411 --- /dev/null +++ b/public/app/plugins/panel/heatmap/css/heatmap.light.css @@ -0,0 +1,58 @@ +.axis { + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: smaller; + fill: #555555; +} + +.axis path, +.axis line { + fill: none; + stroke: #D8D9DA; + /*shape-rendering: crispEdges;*/ +} + +.tick line { + opacity: 0.4; + stroke: #D8D9DA; +} + +.tick text { + fill: #555555; +} + +.heatmap-panel { + cursor: crosshair; +} + +div.heatmap-tooltip { + position: absolute; + text-align: left; + min-width: 160px; + padding: 12px; + font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size: 13px; + background: #ECECEC; + border: 0px; + border-radius: 8px; + pointer-events: none; +} + +.card-highlighted:hover { + stroke: #D8D9DA; +} + +.heatmap-histogram rect { + fill: #555555; +} + +.heatmap-crosshair line { + stroke: #a25959; + stroke-width: 1; +} + +.heatmap-selection { + stroke-width: 1; + opacity: 0.3; + fill: #555555; + stroke: #000; +} diff --git a/public/app/plugins/panel/heatmap/display_editor.ts b/public/app/plugins/panel/heatmap/display_editor.ts new file mode 100644 index 00000000000..775017b00da --- /dev/null +++ b/public/app/plugins/panel/heatmap/display_editor.ts @@ -0,0 +1,26 @@ +/// + +export class HeatmapDisplayEditorCtrl { + panel: any; + panelCtrl: any; + + /** @ngInject */ + constructor($scope) { + $scope.editor = this; + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + + this.panelCtrl.render(); + } +} + +/** @ngInject */ +export function heatmapDisplayEditor() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/app/plugins/panel/heatmap/partials/display_editor.html', + controller: HeatmapDisplayEditorCtrl, + }; +} diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts new file mode 100644 index 00000000000..c4ad9c422dc --- /dev/null +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -0,0 +1,220 @@ +/// + +import {MetricsPanelCtrl} from 'app/plugins/sdk'; +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import TimeSeries from 'app/core/time_series'; +import {axesEditor} from './axes_editor'; +import {heatmapDisplayEditor} from './display_editor'; +import rendering from './rendering'; +import {convertToHeatMap, getMinLog} from './heatmap_data_converter'; + +let X_BUCKET_NUMBER_DEFAULT = 30; +let Y_BUCKET_NUMBER_DEFAULT = 10; + +let panelDefaults = { + heatmap: { + }, + cards: { + cardPadding: null, + cardRound: null + }, + color: { + mode: 'color', + cardColor: '#b4ff00', + colorScale: 'linear', + exponent: 0.5, + colorScheme: 'interpolateSpectral', + fillBackground: false + }, + xBucketSize: null, + xBucketNumber: null, + yBucketSize: null, + yBucketNumber: null, + xAxis: { + show: true + }, + yAxis: { + show: true, + format: 'short', + decimals: null, + logBase: 1, + splitFactor: null, + min: null, + max: null, + removeZeroValues: false + }, + tooltip: { + show: true, + seriesStat: false, + showHistogram: false + }, + highlightCards: true +}; + +let colorModes = ['opacity', 'color']; +let opacityScales = ['linear', 'sqrt']; + +// Schemes from d3-scale-chromatic +// https://github.com/d3/d3-scale-chromatic +let colorSchemes = [ + // Diverging + {name: 'Spectral', value: 'interpolateSpectral'}, + {name: 'BrBG', value: 'interpolateBrBG'}, + {name: 'PRGn', value: 'interpolatePRGn'}, + {name: 'PiYG', value: 'interpolatePiYG'}, + {name: 'PuOr', value: 'interpolatePuOr'}, + {name: 'RdBu', value: 'interpolateRdBu'}, + {name: 'RdGy', value: 'interpolateRdGy'}, + {name: 'RdYlBu', value: 'interpolateRdYlBu'}, + {name: 'RdYlGn', value: 'interpolateRdYlGn'}, + + // Sequential (Single Hue) + {name: 'Blues', value: 'interpolateBlues'}, + {name: 'Greens', value: 'interpolateGreens'}, + {name: 'Greys', value: 'interpolateGreys'}, + {name: 'Oranges', value: 'interpolateOranges'}, + {name: 'Purples', value: 'interpolatePurples'}, + {name: 'Reds', value: 'interpolateReds'}, + + // Sequential (Multi-Hue) + {name: 'BuGn', value: 'interpolateBuGn'}, + {name: 'BuPu', value: 'interpolateBuPu'}, + {name: 'GnBu', value: 'interpolateGnBu'}, + {name: 'OrRd', value: 'interpolateOrRd'}, + {name: 'PuBuGn', value: 'interpolatePuBuGn'}, + {name: 'PuBu', value: 'interpolatePuBu'}, + {name: 'PuRd', value: 'interpolatePuRd'}, + {name: 'RdPu', value: 'interpolateRdPu'}, + {name: 'YlGnBu', value: 'interpolateYlGnBu'}, + {name: 'YlGn', value: 'interpolateYlGn'}, + {name: 'YlOrBr', value: 'interpolateYlOrBr'}, + {name: 'YlOrRd', value: 'interpolateYlOrRd'} +]; + +export class HeatmapCtrl extends MetricsPanelCtrl { + static templateUrl = 'module.html'; + + opacityScales: any = []; + colorModes: any = []; + colorSchemes: any = []; + selectionActivated: boolean; + unitFormats: any; + data: any; + series: any; + timeSrv: any; + + constructor($scope, $injector, private $rootScope, timeSrv) { + super($scope, $injector); + this.$rootScope = $rootScope; + this.timeSrv = timeSrv; + this.selectionActivated = false; + + _.defaultsDeep(this.panel, panelDefaults); + this.opacityScales = opacityScales; + this.colorModes = colorModes; + this.colorSchemes = colorSchemes; + + // Bind grafana panel events + this.events.on('render', this.onRender.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + } + + onInitEditMode() { + this.addEditorTab('Axes', axesEditor, 2); + this.addEditorTab('Display', heatmapDisplayEditor, 3); + this.unitFormats = kbn.getUnitFormats(); + } + + zoomOut(evt) { + this.publishAppEvent('zoom-out', 2); + } + + onRender() { + if (!this.range) { return; } + + let xBucketSize, yBucketSize; + let logBase = this.panel.yAxis.logBase; + let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; + let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); + + // Parse X bucket size (number or interval) + let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); + if (isIntervalString) { + xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); + } else if (isNaN(Number(this.panel.xBucketSize)) || this.panel.xBucketSize === '' || this.panel.xBucketSize === null) { + xBucketSize = xBucketSizeByNumber; + } else { + xBucketSize = Number(this.panel.xBucketSize); + } + + // Calculate Y bucket size + let heatmapStats = this.parseSeries(this.series); + let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + if (logBase !== 1) { + yBucketSize = this.panel.yAxis.splitFactor; + } else { + if (heatmapStats.max === heatmapStats.min) { + yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + } else { + yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; + } + yBucketSize = this.panel.yBucketSize || yBucketSize; + } + + let bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase); + + // Set default Y range if no data + if (!heatmapStats.min && !heatmapStats.max) { + heatmapStats = {min: -1, max: 1, minLog: 1}; + yBucketSize = 1; + } + + this.data = { + buckets: bucketsData, + heatmapStats: heatmapStats, + xBucketSize: xBucketSize, + yBucketSize: yBucketSize + }; + } + + onDataReceived(dataList) { + this.series = dataList.map(this.seriesHandler.bind(this)); + this.render(); + } + + onDataError() { + this.series = []; + this.render(); + } + + seriesHandler(seriesData) { + var series = new TimeSeries({ + datapoints: seriesData.datapoints, + alias: seriesData.target + }); + + series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); + series.minLog = getMinLog(series); + return series; + } + + parseSeries(series) { + let min = _.min(_.map(series, s => s.stats.min)); + let minLog = _.min(_.map(series, s => s.minLog)); + let max = _.max(_.map(series, s => s.stats.max)); + + return { + max: max, + min: min, + minLog: minLog + }; + } + + link(scope, elem, attrs, ctrl) { + rendering(scope, elem, attrs, ctrl); + } +} diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts new file mode 100644 index 00000000000..38d1818a24e --- /dev/null +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -0,0 +1,357 @@ +/// + +import _ from 'lodash'; + +let VALUE_INDEX = 0; +let TIME_INDEX = 1; + +/** + * Convert set of time series into heatmap buckets + * @return {Object} Heatmap object: + * { + * xBucketBound_1: { + * x: xBucketBound_1, + * buckets: { + * yBucketBound_1: { + * y: yBucketBound_1, + * bounds: {bottom, top} + * values: [val_1, val_2, ..., val_K], + * points: [[val_Y, val_X, series_name], ..., [...]], + * seriesStat: {seriesName_1: val_1, seriesName_2: val_2} + * }, + * ... + * yBucketBound_M: {} + * }, + * values: [val_1, val_2, ..., val_K], + * points: [ + * [val_Y, val_X, series_name], (point_1) + * ... + * [...] (point_K) + * ] + * }, + * xBucketBound_2: {}, + * ... + * xBucketBound_N: {} + * } + */ +function convertToHeatMap(series, yBucketSize, xBucketSize, logBase) { + let seriesBuckets = _.map(series, s => { + return seriesToHeatMap(s, yBucketSize, xBucketSize, logBase); + }); + + let buckets = mergeBuckets(seriesBuckets); + return buckets; +} + +/** + * Convert buckets into linear array of "cards" - objects, represented heatmap elements. + * @param {Object} buckets + * @return {Array} Array of "card" objects + */ +function convertToCards(buckets) { + let cards = []; + _.forEach(buckets, xBucket => { + _.forEach(xBucket.buckets, (yBucket, key) => { + if (yBucket.values.length) { + let card = { + x: Number(xBucket.x), + y: Number(key), + yBounds: yBucket.bounds, + values: yBucket.values, + seriesStat: getSeriesStat(yBucket.points) + }; + + cards.push(card); + } + }); + }); + + return cards; +} + +/** + * Special method for log scales. When series converted into buckets with log scale, + * for simplification, 0 values are converted into 0, not into -Infinity. On the other hand, we mean + * that all values less than series minimum, is 0 values, and we create special "minimum" bucket for + * that values (actually, there're no values less than minimum, so this bucket is empty). + * 8-16| | ** | | * | **| + * 4-8| * |* *|* |** *| * | + * 2-4| * *| | ***| |* | + * 1-2|* | | | | | This bucket contains minimum series value + * 0.5-1|____|____|____|____|____| This bucket should be displayed as 0 on graph + * 0|____|____|____|____|____| This bucket is for 0 values (should actually be -Infinity) + * So we should merge two bottom buckets into one (0-value bucket). + * + * @param {Object} buckets Heatmap buckets + * @param {Number} minValue Minimum series value + * @return {Object} Transformed buckets + */ +function mergeZeroBuckets(buckets, minValue) { + _.forEach(buckets, xBucket => { + let yBuckets = xBucket.buckets; + + let emptyBucket = { + bounds: {bottom: 0, top: 0}, + values: [], + points: [] + }; + + let nullBucket = yBuckets[0] || emptyBucket; + let minBucket = yBuckets[minValue] || emptyBucket; + + let newBucket = { + y: 0, + bounds: {bottom: minValue, top: minBucket.bounds.top || minValue}, + values: nullBucket.values.concat(minBucket.values), + points: nullBucket.points.concat(minBucket.points) + }; + + let newYBuckets = {}; + _.forEach(yBuckets, (bucket, bound) => { + bound = Number(bound); + if (bound !== 0 && bound !== minValue) { + newYBuckets[bound] = bucket; + } + }); + newYBuckets[0] = newBucket; + xBucket.buckets = newYBuckets; + }); + + return buckets; +} + +/** + * Remove 0 values from heatmap buckets. + */ +function removeZeroBuckets(buckets) { + _.forEach(buckets, xBucket => { + let yBuckets = xBucket.buckets; + let newYBuckets = {}; + _.forEach(yBuckets, (bucket, bound) => { + if (bucket.y !== 0) { + newYBuckets[bound] = bucket; + } + }); + xBucket.buckets = newYBuckets; + }); + + return buckets; +} + +/** + * Count values number for each timeseries in given bucket + * @param {Array} points Bucket's datapoints with series name ([val, ts, series_name]) + * @return {Object} seriesStat: {seriesName_1: val_1, seriesName_2: val_2} + */ +function getSeriesStat(points) { + return _.countBy(points, p => p[2]); +} + +/** + * Convert individual series to heatmap buckets + */ +function seriesToHeatMap(series, yBucketSize, xBucketSize, logBase = 1) { + let datapoints = series.datapoints; + let seriesName = series.label; + let xBuckets = {}; + + // Slice series into X axis buckets + // | | ** | | * | **| + // | * |* *|* |** *| * | + // |** *| | ***| |* | + // |____|____|____|____|____|_ + // + _.forEach(datapoints, point => { + let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize); + pushToXBuckets(xBuckets, point, bucketBound, seriesName); + }); + + // Slice X axis buckets into Y (value) buckets + // | **| |2|, + // | * | --\ |1|, + // |* | --/ |1|, + // |____| |0| + // + _.forEach(xBuckets, xBucket => { + if (logBase !== 1) { + xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase); + } else { + xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize); + } + }); + return xBuckets; +} + +function pushToXBuckets(buckets, point, bucketNum, seriesName) { + let value = point[VALUE_INDEX]; + if (value === null || value === undefined || isNaN(value)) { return; } + + // Add series name to point for future identification + point.push(seriesName); + + if (buckets[bucketNum] && buckets[bucketNum].values) { + buckets[bucketNum].values.push(value); + buckets[bucketNum].points.push(point); + } else { + buckets[bucketNum] = { + x: bucketNum, + values: [value], + points: [point] + }; + } +} + +function pushToYBuckets(buckets, bucketNum, value, point, bounds) { + if (buckets[bucketNum]) { + buckets[bucketNum].values.push(value); + buckets[bucketNum].points.push(point); + } else { + buckets[bucketNum] = { + y: bucketNum, + bounds: bounds, + values: [value], + points: [point] + }; + } +} + +function getValueBucketBound(value, yBucketSize, logBase) { + if (logBase === 1) { + return getBucketBound(value, yBucketSize); + } else { + return getLogScaleBucketBound(value, yBucketSize, logBase); + } +} + +/** + * Find bucket for given value (for linear scale) + */ +function getBucketBounds(value, bucketSize) { + let bottom, top; + bottom = Math.floor(value / bucketSize) * bucketSize; + top = (Math.floor(value / bucketSize) + 1) * bucketSize; + + return {bottom, top}; +} + +function getBucketBound(value, bucketSize) { + let bounds = getBucketBounds(value, bucketSize); + return bounds.bottom; +} + +function convertToValueBuckets(xBucket, bucketSize) { + let values = xBucket.values; + let points = xBucket.points; + let buckets = {}; + _.forEach(values, (val, index) => { + let bounds = getBucketBounds(val, bucketSize); + let bucketNum = bounds.bottom; + pushToYBuckets(buckets, bucketNum, val, points[index], bounds); + }); + + return buckets; +} + +/** + * Find bucket for given value (for log scales) + */ +function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) { + let top, bottom; + if (value === 0) { + return {bottom: 0, top: 0}; + } + + let value_log = logp(value, logBase); + let pow, powTop; + if (yBucketSplitFactor === 1 || !yBucketSplitFactor) { + pow = Math.floor(value_log); + powTop = pow + 1; + } else { + let additional_bucket_size = 1 / yBucketSplitFactor; + let additional_log = value_log - Math.floor(value_log); + additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size; + pow = Math.floor(value_log) + additional_log; + powTop = pow + additional_bucket_size; + } + bottom = Math.pow(logBase, pow); + top = Math.pow(logBase, powTop); + + return {bottom, top}; +} + +function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) { + let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase); + return bounds.bottom; +} + +function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) { + let values = xBucket.values; + let points = xBucket.points; + + let buckets = {}; + _.forEach(values, (val, index) => { + let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase); + let bucketNum = bounds.bottom; + pushToYBuckets(buckets, bucketNum, val, points[index], bounds); + }); + + return buckets; +} + +/** + * Merge individual buckets for all series into one + * @param {Array} seriesBuckets Array of series buckets + * @return {Object} Merged buckets. + */ +function mergeBuckets(seriesBuckets) { + let mergedBuckets = {}; + _.forEach(seriesBuckets, (seriesBucket, index) => { + if (index === 0) { + mergedBuckets = seriesBucket; + } else { + _.forEach(seriesBucket, (xBucket, xBound) => { + if (mergedBuckets[xBound]) { + mergedBuckets[xBound].points = xBucket.points.concat(mergedBuckets[xBound].points); + mergedBuckets[xBound].values = xBucket.values.concat(mergedBuckets[xBound].values); + + _.forEach(xBucket.buckets, (yBucket, yBound) => { + let bucket = mergedBuckets[xBound].buckets[yBound]; + if (bucket && bucket.values) { + mergedBuckets[xBound].buckets[yBound].values = bucket.values.concat(yBucket.values); + mergedBuckets[xBound].buckets[yBound].points = bucket.points.concat(yBucket.points); + } else { + mergedBuckets[xBound].buckets[yBound] = yBucket; + } + + let points = mergedBuckets[xBound].buckets[yBound].points; + mergedBuckets[xBound].buckets[yBound].seriesStat = getSeriesStat(points); + }); + } else { + mergedBuckets[xBound] = xBucket; + } + }); + } + }); + + return mergedBuckets; +} + +// Get minimum non zero value. +function getMinLog(series) { + let values = _.compact(_.map(series.datapoints, p => p[0])); + return _.min(values); +} + +// Logarithm for custom base +function logp(value, base) { + return Math.log(value) / Math.log(base); +} + +export { + convertToHeatMap, + convertToCards, + removeZeroBuckets, + mergeZeroBuckets, + getMinLog, + getValueBucketBound +}; diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts new file mode 100644 index 00000000000..62d03a503c3 --- /dev/null +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -0,0 +1,248 @@ +/// + +import d3 from 'd3'; +import $ from 'jquery'; +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; +import {getValueBucketBound} from './heatmap_data_converter'; + +let TOOLTIP_PADDING_X = 30; +let TOOLTIP_PADDING_Y = 5; +let HISTOGRAM_WIDTH = 160; +let HISTOGRAM_HEIGHT = 40; + +export class HeatmapTooltip { + tooltip: any; + scope: any; + dashboard: any; + panel: any; + heatmapPanel: any; + mouseOverBucket: boolean; + originalFillColor: any; + + constructor(elem, scope) { + this.scope = scope; + this.dashboard = scope.ctrl.dashboard; + this.panel = scope.ctrl.panel; + this.heatmapPanel = elem; + this.mouseOverBucket = false; + this.originalFillColor = null; + + elem.on("mouseover", this.onMouseOver.bind(this)); + elem.on("mouseleave", this.onMouseLeave.bind(this)); + } + + onMouseOver(e) { + if (!this.tooltip) { + this.add(); + this.move(e); + } + } + + onMouseLeave() { + this.destroy(); + } + + onMouseMove(e) { + if (!this.panel.tooltip.show) { return; } + + this.move(e); + } + + add() { + this.tooltip = d3.select("body") + .append("div") + .attr("class", "heatmap-tooltip"); + } + + destroy() { + if (this.tooltip) { + this.tooltip.remove(); + } + + this.tooltip = null; + } + + show(pos, data) { + if (!this.panel.tooltip.show || !data) { return; } + + let {xBucketIndex, yBucketIndex} = this.getBucketIndexes(pos, data); + + if (!data.buckets[xBucketIndex] || !this.tooltip) { + this.destroy(); + return; + } + + let boundBottom, boundTop, valuesNumber; + let xData = data.buckets[xBucketIndex]; + let yData = xData.buckets[yBucketIndex]; + + let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; + let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); + let decimals = this.panel.tooltipDecimals || 5; + let valueFormatter = this.valueFormatter(decimals); + + let tooltipHtml = `
${time}
+
`; + + if (yData) { + boundBottom = valueFormatter(yData.bounds.bottom); + boundTop = valueFormatter(yData.bounds.top); + valuesNumber = yData.values.length; + tooltipHtml += `
+ bucket: ${boundBottom} - ${boundTop}
+ values: ${valuesNumber}
+
`; + + if (this.panel.tooltip.seriesStat && yData.seriesStat) { + tooltipHtml = this.addSeriesStat(tooltipHtml, yData.seriesStat); + } + } else { + if (!this.panel.tooltip.showHistogram) { + this.destroy(); + return; + } + boundBottom = yBucketIndex; + boundTop = ''; + valuesNumber = 0; + } + + this.tooltip.html(tooltipHtml); + + if (this.panel.tooltip.showHistogram) { + this.addHistogram(xData); + } + + this.move(pos); + } + + getBucketIndexes(pos, data) { + let xBucketIndex, yBucketIndex; + + // if panelRelY is defined another panel wants us to show a tooltip + if (pos.panelRelY) { + xBucketIndex = getValueBucketBound(pos.x, data.xBucketSize, 1); + let y = this.scope.yScale.invert(pos.panelRelY * this.scope.chartHeight); + yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); + pos = this.getSharedTooltipPos(pos); + + if (!this.tooltip) { + // Add shared tooltip for panel + this.add(); + } + } else { + xBucketIndex = this.getXBucketIndex(pos.offsetX, data); + yBucketIndex = this.getYBucketIndex(pos.offsetY, data); + } + + return {xBucketIndex, yBucketIndex}; + } + + getXBucketIndex(offsetX, data) { + let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf(); + let xBucketIndex = getValueBucketBound(x, data.xBucketSize, 1); + return xBucketIndex; + } + + getYBucketIndex(offsetY, data) { + let y = this.scope.yScale.invert(offsetY - this.scope.chartTop); + let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); + return yBucketIndex; + } + + getSharedTooltipPos(pos) { + // get pageX from position on x axis and pageY from relative position in original panel + pos.pageX = this.heatmapPanel.offset().left + this.scope.xScale(pos.x); + pos.pageY = this.heatmapPanel.offset().top + this.scope.chartHeight * pos.panelRelY; + return pos; + } + + addSeriesStat(tooltipHtml, seriesStat) { + tooltipHtml += "series:
"; + _.forEach(seriesStat, (values, series) => { + tooltipHtml += `  -  ${series}: ${values}
`; + }); + + return tooltipHtml; + } + + addHistogram(data) { + let xBucket = this.scope.ctrl.data.buckets[data.x]; + let yBucketSize = this.scope.ctrl.data.yBucketSize; + let {min, max, ticks} = this.scope.ctrl.data.yAxis; + let histogramData = _.map(xBucket.buckets, bucket => { + return [bucket.y, bucket.values.length]; + }); + histogramData = _.filter(histogramData, d => { + return d[0] >= min && d[0] <= max; + }); + + let scale = this.scope.yScale.copy(); + let histXScale = scale + .domain([min, max]) + .range([0, HISTOGRAM_WIDTH]); + + let barWidth; + if (this.panel.yAxis.logBase === 1) { + barWidth = Math.floor(HISTOGRAM_WIDTH / (max - min) * yBucketSize * 0.9); + } else { + barWidth = Math.floor(HISTOGRAM_WIDTH / ticks / yBucketSize * 0.9); + } + barWidth = Math.max(barWidth, 1); + + let histYScale = d3.scaleLinear() + .domain([0, _.max(_.map(histogramData, d => d[1]))]) + .range([0, HISTOGRAM_HEIGHT]); + + let histogram = this.tooltip.select(".heatmap-histogram") + .append("svg") + .attr("width", HISTOGRAM_WIDTH) + .attr("height", HISTOGRAM_HEIGHT); + + histogram.selectAll(".bar").data(histogramData) + .enter().append("rect") + .attr("x", d => { + return histXScale(d[0]); + }) + .attr("width", barWidth) + .attr("y", d => { + return HISTOGRAM_HEIGHT - histYScale(d[1]); + }) + .attr("height", d => { + return histYScale(d[1]); + }); + } + + move(pos) { + if (!this.tooltip) { return; } + + let elem = $(this.tooltip.node())[0]; + let tooltipWidth = elem.clientWidth; + let tooltipHeight = elem.clientHeight; + + let left = pos.pageX + TOOLTIP_PADDING_X; + let top = pos.pageY + TOOLTIP_PADDING_Y; + + if (pos.pageX + tooltipWidth + 40 > 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.tooltip + .style("left", left + "px") + .style("top", top + "px"); + } + + valueFormatter(decimals) { + let format = this.panel.yAxis.format; + return function(value) { + if (_.isInteger(value)) { + decimals = 0; + } + return kbn.valueFormats[format](value, decimals); + }; + } +} diff --git a/public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg b/public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg new file mode 100644 index 00000000000..29838f76ca6 --- /dev/null +++ b/public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg @@ -0,0 +1,195 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html new file mode 100644 index 00000000000..d88681a38b8 --- /dev/null +++ b/public/app/plugins/panel/heatmap/module.html @@ -0,0 +1,7 @@ +
+
+
+
+ +
+
diff --git a/public/app/plugins/panel/heatmap/module.ts b/public/app/plugins/panel/heatmap/module.ts new file mode 100644 index 00000000000..2715b95f916 --- /dev/null +++ b/public/app/plugins/panel/heatmap/module.ts @@ -0,0 +1,13 @@ +/// + +import {loadPluginCss} from 'app/plugins/sdk'; +import {HeatmapCtrl} from './heatmap_ctrl'; + +// loadPluginCss({ +// dark: 'public/app/plugins/panel/heatmap/css/heatmap.dark.css', +// light: 'public/app/plugins/panel/heatmap/css/heatmap.light.css' +// }); + +export { + HeatmapCtrl as PanelCtrl +}; diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html new file mode 100644 index 00000000000..f9a0464d381 --- /dev/null +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -0,0 +1,85 @@ +
+
+
Y Axis
+ + +
+ +
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ + +
+
+ +
+
X Axis
+ + +
+ + +
+
+ + +
+
+
diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html new file mode 100644 index 00000000000..530a7ecbcf9 --- /dev/null +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -0,0 +1,93 @@ +
+
+
Colors
+
+ +
+ +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+ + +
+ + +
+
+ +
+
Cards
+
+ + +
+
+ + +
+
+ +
+
Tooltip
+ + +
+ + + + + + +
+ + +
+
+
+
diff --git a/public/app/plugins/panel/heatmap/plugin.json b/public/app/plugins/panel/heatmap/plugin.json new file mode 100644 index 00000000000..bee509467a7 --- /dev/null +++ b/public/app/plugins/panel/heatmap/plugin.json @@ -0,0 +1,16 @@ +{ + "type": "panel", + "name": "Heatmap", + "id": "heatmap", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-heatmap-panel.svg", + "large": "img/icn-heatmap-panel.svg" + } + } +} diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts new file mode 100644 index 00000000000..9c5b36b8857 --- /dev/null +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -0,0 +1,861 @@ +/// + +import _ from 'lodash'; +import $ from 'jquery'; +import moment from 'moment'; +import kbn from 'app/core/utils/kbn'; +import {appEvents} from 'app/core/core'; +import d3 from 'd3'; +import {HeatmapTooltip} from './heatmap_tooltip'; +import {convertToCards, mergeZeroBuckets, removeZeroBuckets} from './heatmap_data_converter'; + +let MIN_CARD_SIZE = 1, + CARD_PADDING = 1, + CARD_ROUND = 0, + DATA_RANGE_WIDING_FACTOR = 1.2, + DEFAULT_X_TICK_SIZE_PX = 100, + DEFAULT_Y_TICK_SIZE_PX = 50, + X_AXIS_TICK_PADDING = 10, + Y_AXIS_TICK_PADDING = 5, + MIN_SELECTION_WIDTH = 2; + +export default function link(scope, elem, attrs, ctrl) { + let data, timeRange, panel, heatmap; + + // $heatmap is JQuery object, but heatmap is D3 + let $heatmap = elem.find('.heatmap-panel'); + let tooltip = new HeatmapTooltip($heatmap, scope); + + let width, height, + yScale, xScale, + chartWidth, chartHeight, + chartTop, chartBottom, + yAxisWidth, xAxisHeight, + cardPadding, cardRound, + cardWidth, cardHeight, + colorScale, opacityScale, + mouseUpHandler; + + let selection = { + active: false, + x1: -1, + x2: -1 + }; + + let padding = {left: 0, right: 0, top: 0, bottom: 0}, + margin = {left: 25, right: 15, top: 10, bottom: 20}, + dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; + + ctrl.events.on('render', () => { + render(); + ctrl.renderingCompleted(); + }); + + function setElementHeight() { + try { + var height = ctrl.height || panel.height || ctrl.row.height; + if (_.isString(height)) { + height = parseInt(height.replace('px', ''), 10); + } + + height -= 5; // padding + height -= panel.title ? 24 : 9; // subtract panel title bar + + $heatmap.css('height', height + 'px'); + + return true; + } catch (e) { // IE throws errors sometimes + return false; + } + } + + function getYAxisWidth(elem) { + let axis_text = elem.selectAll(".axis-y text").nodes(); + let max_text_width = _.max(_.map(axis_text, text => { + let el = $(text); + // Use JQuery outerWidth() to compute full element width + return el.outerWidth(); + })); + + return max_text_width; + } + + function getXAxisHeight(elem) { + let axis_line = elem.select(".axis-x line"); + if (!axis_line.empty()) { + let axis_line_position = parseFloat(elem.select(".axis-x line").attr("y2")); + let canvas_width = parseFloat(elem.attr("height")); + return canvas_width - axis_line_position; + } else { + // Default height + return 30; + } + } + + function addXAxis() { + xScale = d3.scaleTime() + .domain([timeRange.from, timeRange.to]) + .range([0, chartWidth]); + + let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; + let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + + let xAxis = d3.axisBottom(xScale) + .ticks(ticks) + .tickFormat(d3.timeFormat(grafanaTimeFormatter)) + .tickPadding(X_AXIS_TICK_PADDING) + .tickSize(chartHeight); + + let posY = margin.top; + let posX = yAxisWidth; + heatmap.append("g") + .attr("class", "axis axis-x") + .attr("transform", "translate(" + posX + "," + posY + ")") + .call(xAxis); + + // Remove horizontal line in the top of axis labels (called domain in d3) + heatmap.select(".axis-x").select(".domain").remove(); + } + + function addYAxis() { + let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX); + let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks); + let {y_min, y_max} = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval); + + // Rewrite min and max if it have been set explicitly + y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min; + y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max; + + // Adjust ticks after Y range widening + tick_interval = tickStep(y_min, y_max, ticks); + ticks = Math.ceil((y_max - y_min) / tick_interval); + + let decimals = panel.yAxis.decimals === null ? getPrecision(tick_interval) : panel.yAxis.decimals; + + // Set default Y min and max if no data + if (_.isEmpty(data.buckets)) { + y_max = 1; + y_min = -1; + ticks = 3; + decimals = 1; + } + + data.yAxis = { + min: y_min, + max: y_max, + ticks: ticks + }; + + yScale = d3.scaleLinear() + .domain([y_min, y_max]) + .range([chartHeight, 0]); + + let yAxis = d3.axisLeft(yScale) + .ticks(ticks) + .tickFormat(tickValueFormatter(decimals)) + .tickSizeInner(0 - width) + .tickSizeOuter(0) + .tickPadding(Y_AXIS_TICK_PADDING); + + heatmap.append("g") + .attr("class", "axis axis-y") + .call(yAxis); + + // Calculate Y axis width first, then move axis into visible area + let posY = margin.top; + let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")"); + + // Remove vertical line in the right of axis labels (called domain in d3) + heatmap.select(".axis-y").select(".domain").remove(); + } + + // Wide Y values range and anjust to bucket size + function wideYAxisRange(min, max, tickInterval) { + let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2; + let y_min, y_max; + + if (tickInterval === 0) { + y_max = max * dataRangeWidingFactor; + y_min = min - min * (dataRangeWidingFactor - 1); + tickInterval = (y_max - y_min) / 2; + } else { + y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval; + y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval; + } + + // Don't wide axis below 0 if all values are positive + if (min >= 0 && y_min < 0) { + y_min = 0; + } + + return {y_min, y_max}; + } + + function addLogYAxis() { + let log_base = panel.yAxis.logBase; + let {y_min, y_max} = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base); + + y_min = panel.yAxis.min !== null ? adjustLogMin(panel.yAxis.min, log_base) : y_min; + y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max; + + // Set default Y min and max if no data + if (_.isEmpty(data.buckets)) { + y_max = Math.pow(log_base, 2); + y_min = 1; + } + + yScale = d3.scaleLog() + .base(panel.yAxis.logBase) + .domain([y_min, y_max]) + .range([chartHeight, 0]); + + let domain = yScale.domain(); + let tick_values = logScaleTickValues(domain, log_base); + let decimals = panel.yAxis.decimals; + + data.yAxis = { + min: y_min, + max: y_max, + ticks: tick_values.length + }; + + let yAxis = d3.axisLeft(yScale) + .tickValues(tick_values) + .tickFormat(tickValueFormatter(decimals)) + .tickSizeInner(0 - width) + .tickSizeOuter(0) + .tickPadding(Y_AXIS_TICK_PADDING); + + heatmap.append("g") + .attr("class", "axis axis-y") + .call(yAxis); + + // Calculate Y axis width first, then move axis into visible area + let posY = margin.top; + let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")"); + + // Set first tick as pseudo 0 + if (y_min < 1) { + heatmap.select(".axis-y").select(".tick text").text("0"); + } + + // Remove vertical line in the right of axis labels (called domain in d3) + heatmap.select(".axis-y").select(".domain").remove(); + } + + // Adjust data range to log base + function adjustLogRange(min, max, logBase) { + let y_min, y_max; + + y_min = data.heatmapStats.minLog; + if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) { + y_min = 1; + } else { + y_min = adjustLogMin(data.heatmapStats.minLog, logBase); + } + + // Adjust max Y value to log base + y_max = adjustLogMax(data.heatmapStats.max, logBase); + + return {y_min, y_max}; + } + + function adjustLogMax(max, base) { + return Math.pow(base, Math.ceil(logp(max, base))); + } + + function adjustLogMin(min, base) { + return Math.pow(base, Math.floor(logp(min, base))); + } + + function logScaleTickValues(domain, base) { + let domainMin = domain[0]; + let domainMax = domain[1]; + let tickValues = []; + + if (domainMin < 1) { + let under_one_ticks = Math.floor(logp(domainMin, base)); + for (let i = under_one_ticks; i < 0; i++) { + let tick_value = Math.pow(base, i); + tickValues.push(tick_value); + } + } + + let ticks = Math.ceil(logp(domainMax, base)); + for (let i = 0; i <= ticks; i++) { + let tick_value = Math.pow(base, i); + tickValues.push(tick_value); + } + + return tickValues; + } + + function tickValueFormatter(decimals) { + let format = panel.yAxis.format; + return function(value) { + return kbn.valueFormats[format](value, decimals); + }; + } + + function fixYAxisTickSize() { + heatmap.select(".axis-y") + .selectAll(".tick line") + .attr("x2", chartWidth); + } + + function addAxes() { + chartHeight = height - margin.top - margin.bottom; + chartTop = margin.top; + chartBottom = chartTop + chartHeight; + + if (panel.yAxis.logBase === 1) { + addYAxis(); + } else { + addLogYAxis(); + } + + yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + chartWidth = width - yAxisWidth - margin.right; + fixYAxisTickSize(); + + addXAxis(); + xAxisHeight = getXAxisHeight(heatmap); + + if (!panel.yAxis.show) { + heatmap.select(".axis-y").selectAll("line").style("opacity", 0); + } + + if (!panel.xAxis.show) { + heatmap.select(".axis-x").selectAll("line").style("opacity", 0); + } + } + + function addHeatmapCanvas() { + let heatmap_elem = $heatmap[0]; + width = heatmap_elem.clientWidth - padding.right; + height = heatmap_elem.clientHeight - padding.bottom; + cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING; + cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND; + + if (heatmap) { + heatmap.remove(); + } + + heatmap = d3.select(heatmap_elem) + .append("svg") + .attr("width", width) + .attr("height", height); + } + + function addHeatmap() { + addHeatmapCanvas(); + addAxes(); + + if (panel.yAxis.logBase !== 1) { + if (panel.yAxis.removeZeroValues) { + data.buckets = removeZeroBuckets(data.buckets); + } else { + let log_base = panel.yAxis.logBase; + let domain = yScale.domain(); + let tick_values = logScaleTickValues(domain, log_base); + data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values)); + } + } + let cardsData = convertToCards(data.buckets); + + let max_value = d3.max(cardsData, card => { + return card.values.length; + }); + + setColorScale(max_value); + setOpacityScale(max_value); + setCardSize(); + + if (panel.color.fillBackground && panel.color.mode === 'color') { + fillBackground(heatmap, colorScale(0)); + } + + let cards = heatmap.selectAll(".heatmap-card").data(cardsData); + cards.append("title"); + cards = cards.enter().append("rect") + .attr("x", getCardX) + .attr("width", getCardWidth) + .attr("y", getCardY) + .attr("height", getCardHeight) + .attr("rx", cardRound) + .attr("ry", cardRound) + .attr("class", "bordered heatmap-card") + .style("fill", getCardColor) + .style("stroke", getCardColor) + .style("stroke-width", 0) + .style("opacity", getCardOpacity); + + let $cards = $heatmap.find(".heatmap-card"); + $cards.on("mouseenter", (event) => { + tooltip.mouseOverBucket = true; + highlightCard(event); + }) + .on("mouseleave", (event) => { + tooltip.mouseOverBucket = false; + resetCardHighLight(event); + }); + } + + function highlightCard(event) { + if (panel.highlightCards) { + let color = d3.select(event.target).style("fill"); + let highlightColor = d3.color(color).darker(2); + let strokeColor = d3.color(color).brighter(4); + let current_card = d3.select(event.target); + tooltip.originalFillColor = color; + current_card.style("fill", highlightColor) + .style("stroke", strokeColor) + .style("stroke-width", 1); + } + } + + function resetCardHighLight(event) { + if (panel.highlightCards) { + d3.select(event.target).style("fill", tooltip.originalFillColor) + .style("stroke", tooltip.originalFillColor) + .style("stroke-width", 0); + } + } + + function setColorScale(max_value) { + let colorInterpolator = d3[panel.color.colorScheme]; + colorScale = d3.scaleSequential(colorInterpolator).domain([0, max_value]); + } + + function setOpacityScale(max_value) { + if (panel.color.colorScale === 'linear') { + opacityScale = d3.scaleLinear() + .domain([0, max_value]) + .range([0, 1]); + } else if (panel.color.colorScale === 'sqrt') { + opacityScale = d3.scalePow().exponent(panel.color.exponent) + .domain([0, max_value]) + .range([0, 1]); + } + } + + function setCardSize() { + let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0)); + let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize)); + + if (panel.yAxis.logBase !== 1) { + let base = panel.yAxis.logBase; + let splitFactor = panel.yAxis.splitFactor || 1; + yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor); + } + + cardWidth = xGridSize - cardPadding * 2; + cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0; + } + + function getCardX(d) { + let x; + if (xScale(d.x) < 0) { + // Cut card left to prevent overlay + x = yAxisWidth + cardPadding; + } else { + x = xScale(d.x) + yAxisWidth + cardPadding; + } + + return x; + } + + function getCardWidth(d) { + let w; + if (xScale(d.x) < 0) { + // Cut card left to prevent overlay + let cutted_width = xScale(d.x) + cardWidth; + w = cutted_width > 0 ? cutted_width : 0; + } else if (xScale(d.x) + cardWidth > chartWidth) { + // Cut card right to prevent overlay + w = chartWidth - xScale(d.x) - cardPadding; + } else { + w = cardWidth; + } + + // Card width should be MIN_CARD_SIZE at least + w = Math.max(w, MIN_CARD_SIZE); + return w; + } + + function getCardY(d) { + let y = yScale(d.y) + chartTop - cardHeight - cardPadding; + if (panel.yAxis.logBase !== 1 && d.y === 0) { + y = chartBottom - cardHeight - cardPadding; + } else { + if (y < chartTop) { + y = chartTop; + } + } + + return y; + } + + function getCardHeight(d) { + let y = yScale(d.y) + chartTop - cardHeight - cardPadding; + let h = cardHeight; + + if (panel.yAxis.logBase !== 1 && d.y === 0) { + return cardHeight; + } + + // Cut card height to prevent overlay + if (y < chartTop) { + h = yScale(d.y) - cardPadding; + } else if (yScale(d.y) > chartBottom) { + h = chartBottom - y; + } else if (y + cardHeight > chartBottom) { + h = chartBottom - y; + } + + // Height can't be more than chart height + h = Math.min(h, chartHeight); + // Card height should be MIN_CARD_SIZE at least + h = Math.max(h, MIN_CARD_SIZE); + + return h; + } + + function getCardColor(d) { + if (panel.color.mode === 'opacity') { + return panel.color.cardColor; + } else { + return colorScale(d.values.length); + } + } + + function getCardOpacity(d) { + if (panel.color.mode === 'opacity') { + return opacityScale(d.values.length); + } else { + return 1; + } + } + + function fillBackground(heatmap, color) { + heatmap.insert("rect", "g") + .attr("x", yAxisWidth) + .attr("y", margin.top) + .attr("width", chartWidth) + .attr("height", chartHeight) + .attr("fill", color); + } + + ///////////////////////////// + // Selection and crosshair // + ///////////////////////////// + + // Shared crosshair and tooltip + appEvents.on('graph-hover', event => { + drawSharedCrosshair(event.pos); + + // Show shared tooltip + if (ctrl.dashboard.graphTooltip === 2) { + tooltip.show(event.pos, data); + } + }); + + appEvents.on('graph-hover-clear', () => { + clearCrosshair(); + tooltip.destroy(); + }); + + function onMouseDown(event) { + selection.active = true; + selection.x1 = event.offsetX; + + mouseUpHandler = function() { + onMouseUp(); + }; + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp() { + $(document).unbind("mouseup", mouseUpHandler); + mouseUpHandler = null; + selection.active = false; + + let selectionRange = Math.abs(selection.x2 - selection.x1); + if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { + let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth); + let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth); + + ctrl.timeSrv.setTime({ + from: moment.utc(timeFrom), + to: moment.utc(timeTo) + }); + } + + clearSelection(); + } + + function onMouseLeave() { + appEvents.emit('graph-hover-clear'); + clearCrosshair(); + } + + function onMouseMove(event) { + if (!heatmap) { return; } + + if (selection.active) { + // Clear crosshair and tooltip + clearCrosshair(); + tooltip.destroy(); + + selection.x2 = limitSelection(event.offsetX); + drawSelection(selection.x1, selection.x2); + } else { + emitGraphHoverEvet(event); + drawCrosshair(event.offsetX); + tooltip.show(event, data); + } + } + + function emitGraphHoverEvet(event) { + let x = xScale.invert(event.offsetX - yAxisWidth).valueOf(); + let y = yScale.invert(event.offsetY); + let pos = { + pageX: event.pageX, + pageY: event.pageY, + x: x, x1: x, + y: y, y1: y, + panelRelY: null + }; + + // Set minimum offset to prevent showing legend from another panel + pos.panelRelY = Math.max(event.offsetY / height, 0.001); + + // broadcast to other graph panels that we are hovering + appEvents.emit('graph-hover', {pos: pos, panel: panel}); + } + + function limitSelection(x2) { + x2 = Math.max(x2, yAxisWidth); + x2 = Math.min(x2, chartWidth + yAxisWidth); + return x2; + } + + function drawSelection(posX1, posX2) { + if (heatmap) { + heatmap.selectAll(".heatmap-selection").remove(); + let selectionX = Math.min(posX1, posX2); + let selectionWidth = Math.abs(posX1 - posX2); + + if (selectionWidth > MIN_SELECTION_WIDTH) { + heatmap.append("rect") + .attr("class", "heatmap-selection") + .attr("x", selectionX) + .attr("width", selectionWidth) + .attr("y", chartTop) + .attr("height", chartHeight); + } + } + } + + function clearSelection() { + selection.x1 = -1; + selection.x2 = -1; + + if (heatmap) { + heatmap.selectAll(".heatmap-selection").remove(); + } + } + + function drawCrosshair(position) { + if (heatmap) { + heatmap.selectAll(".heatmap-crosshair").remove(); + + let posX = position; + posX = Math.max(posX, yAxisWidth); + posX = Math.min(posX, chartWidth + yAxisWidth); + + heatmap.append("g") + .attr("class", "heatmap-crosshair") + .attr("transform", "translate(" + posX + ",0)") + .append("line") + .attr("x1", 1) + .attr("y1", chartTop) + .attr("x2", 1) + .attr("y2", chartBottom) + .attr("stroke-width", 1); + } + } + + function drawSharedCrosshair(pos) { + if (heatmap && ctrl.dashboard.graphTooltip !== 0) { + let posX = xScale(pos.x) + yAxisWidth; + drawCrosshair(posX); + } + } + + function clearCrosshair() { + if (heatmap) { + heatmap.selectAll(".heatmap-crosshair").remove(); + } + } + + function drawColorLegend() { + d3.select("#heatmap-color-legend").selectAll("rect").remove(); + + let legend = d3.select("#heatmap-color-legend"); + let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth()); + let legendHeight = d3.select("#heatmap-color-legend").attr("height"); + + let colorInterpolator = d3[panel.color.colorScheme]; + let legendColorScale = d3.scaleSequential(colorInterpolator).domain([0, legendWidth]); + + let rangeStep = 2; + let valuesRange = d3.range(0, legendWidth, rangeStep); + var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => { + return legendColorScale(d); + }); + } + + function drawOpacityLegend() { + d3.select("#heatmap-opacity-legend").selectAll("rect").remove(); + + let legend = d3.select("#heatmap-opacity-legend"); + let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth()); + let legendHeight = d3.select("#heatmap-opacity-legend").attr("height"); + + let legendOpacityScale; + if (panel.color.colorScale === 'linear') { + legendOpacityScale = d3.scaleLinear() + .domain([0, legendWidth]) + .range([0, 1]); + } else if (panel.color.colorScale === 'sqrt') { + legendOpacityScale = d3.scalePow().exponent(panel.color.exponent) + .domain([0, legendWidth]) + .range([0, 1]); + } + + let rangeStep = 1; + let valuesRange = d3.range(0, legendWidth, rangeStep); + var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", panel.color.cardColor) + .style("opacity", d => { + return legendOpacityScale(d); + }); + } + + function render() { + if (!ctrl.data || _.isEmpty(ctrl.data.buckets)) { return; } + + data = ctrl.data; + panel = ctrl.panel; + timeRange = ctrl.range; + + if (setElementHeight()) { + addHeatmap(); + scope.yScale = yScale; + scope.xScale = xScale; + scope.yAxisWidth = yAxisWidth; + scope.xAxisHeight = xAxisHeight; + scope.chartHeight = chartHeight; + scope.chartWidth = chartWidth; + scope.chartTop = chartTop; + + // Register selection listeners + $heatmap.on("mousedown", onMouseDown); + $heatmap.on("mousemove", onMouseMove); + $heatmap.on("mouseleave", onMouseLeave); + } + + // Draw only if color editor is opened + if (!d3.select("#heatmap-color-legend").empty()) { + drawColorLegend(); + } + if (!d3.select("#heatmap-opacity-legend").empty()) { + drawOpacityLegend(); + } + } +} + +function grafanaTimeFormat(ticks, min, max) { + if (min && max && ticks) { + let range = max - min; + let secPerTick = (range/ticks) / 1000; + let oneDay = 86400000; + let oneYear = 31536000000; + + if (secPerTick <= 45) { + return "%H:%M:%S"; + } + if (secPerTick <= 7200 || range <= oneDay) { + return "%H:%M"; + } + if (secPerTick <= 80000) { + return "%m/%d %H:%M"; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return "%m/%d"; + } + return "%Y-%m"; + } + + return "%H:%M"; +} + +// Calculate tick step. +// Implementation from d3-array (ticks.js) +// https://github.com/d3/d3-array/blob/master/src/ticks.js +function tickStep(start, stop, count) { + var e10 = Math.sqrt(50), + e5 = Math.sqrt(10), + e2 = Math.sqrt(2); + + var step0 = Math.abs(stop - start) / Math.max(0, count), + step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), + error = step0 / step1; + + if (error >= e10) { + step1 *= 10; + } else if (error >= e5) { + step1 *= 5; + } else if (error >= e2) { + step1 *= 2; + } + + return stop < start ? -step1 : step1; +} + +function logp(value, base) { + return Math.log(value) / Math.log(base); +} + +function getPrecision(num) { + let str = num.toString(); + let dot_index = str.indexOf("."); + if (dot_index === -1) { + return 0; + } else { + return str.length - dot_index - 1; + } +} + +function getTicksPrecision(values) { + let precisions = _.map(values, getPrecision); + return _.max(precisions); +} diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 4574c56742a..f7ed0c5b2d2 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -30,7 +30,8 @@ System.config({ "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", - "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", + "d3": "vendor/d3/d3.js" }, packages: { diff --git a/public/vendor/d3/LICENSE b/public/vendor/d3/LICENSE new file mode 100644 index 00000000000..721bd22ece6 --- /dev/null +++ b/public/vendor/d3/LICENSE @@ -0,0 +1,27 @@ +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/vendor/d3/README.md b/public/vendor/d3/README.md new file mode 100644 index 00000000000..32299bfe579 --- /dev/null +++ b/public/vendor/d3/README.md @@ -0,0 +1,57 @@ +# D3: Data-Driven Documents + + + +**D3** (or **D3.js**) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data. + +## Resources + +* [API Reference](https://github.com/d3/d3/blob/master/API.md) +* [Release Notes](https://github.com/d3/d3/releases) +* [Gallery](https://github.com/d3/d3/wiki/Gallery) +* [Examples](http://bl.ocks.org/mbostock) +* [Wiki](https://github.com/d3/d3/wiki) + +## Installing + +If you use npm, `npm install d3`. Otherwise, download the [latest release](https://github.com/d3/d3/releases/latest). The released bundle supports anonymous AMD, CommonJS, and vanilla environments. You can load directly from [d3js.org](https://d3js.org), [CDNJS](https://cdnjs.com/libraries/d3), or [unpkg](https://unpkg.com/d3/). For example: + +```html + +``` + +For the minified version: + +```html + +``` + +You can also use the standalone D3 microlibraries. For example, [d3-selection](https://github.com/d3/d3-selection): + +```html + +``` + +D3 is written using [ES2015 modules](http://www.2ality.com/2014/09/es6-modules-final.html). Create a [custom bundle using Rollup](http://bl.ocks.org/mbostock/bb09af4c39c79cffcde4), Webpack, or your preferred bundler. To import D3 into an ES2015 application, either import specific symbols from specific D3 modules: + +```js +import {scaleLinear} from "d3-scale"; +``` + +Or import everything into a namespace (here, `d3`): + +```js +import * as d3 from "d3"; +``` + +In Node: + +```js +var d3 = require("d3"); +``` + +You can also require individual modules and combine them into a `d3` object using [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign): + +```js +var d3 = Object.assign({}, require("d3-format"), require("d3-geo"), require("d3-geo-projection")); +``` diff --git a/public/vendor/d3/d3-scale-chromatic.min.js b/public/vendor/d3/d3-scale-chromatic.min.js new file mode 100644 index 00000000000..86aed948666 --- /dev/null +++ b/public/vendor/d3/d3-scale-chromatic.min.js @@ -0,0 +1,2 @@ +// https://d3js.org/d3-scale-chromatic/ Version 1.1.1. Copyright 2017 Mike Bostock. +!function(f,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("./d3.v4.min.js")):"function"==typeof define&&define.amd?define(["exports","./d3.v4.min.js"],e):e(f.d3=f.d3||{},f.d3)}(this,function(f,e){"use strict";var d=function(f){for(var e=f.length/6|0,d=new Array(e),c=0;c=Ys?i*=10:o>=Bs?i*=5:o>=js&&(i*=2),n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function _(t,n){for(var e,r=0,i=t.length;r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function N(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function q(t){return function(){this.removeAttribute(t)}}function L(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function D(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function O(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function F(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function I(t){return function(){this.style.removeProperty(t)}}function Y(t,n,e){return function(){this.style.setProperty(t,n,e)}}function B(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function j(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function X(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function V(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new $(t)}function $(t){this._node=t,this._names=V(t.getAttribute("class")||"")}function Z(t,n){for(var e=W(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=El.exec(t))?Tt(parseInt(n[1],16)):(n=Al.exec(t))?new Et(n[1],n[2],n[3],1):(n=Cl.exec(t))?new Et(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=zl.exec(t))?Nt(n[1],n[2],n[3],n[4]):(n=Pl.exec(t))?Nt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Rl.exec(t))?At(n[1],n[2]/100,n[3]/100,1):(n=ql.exec(t))?At(n[1],n[2]/100,n[3]/100,n[4]):Ll.hasOwnProperty(t)?Tt(Ll[t]):"transparent"===t?new Et(NaN,NaN,NaN,0):null}function Tt(t){return new Et(t>>16&255,t>>8&255,255&t,1)}function Nt(t,n,e,r){return r<=0&&(t=n=e=NaN),new Et(t,n,e,r)}function kt(t){return t instanceof wt||(t=Mt(t)),t?(t=t.rgb(),new Et(t.r,t.g,t.b,t.opacity)):new Et}function St(t,n,e,r){return 1===arguments.length?kt(t):new Et(t,n,e,null==r?1:r)}function Et(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function At(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Pt(t,n,e,r)}function Ct(t){if(t instanceof Pt)return new Pt(t.h,t.s,t.l,t.opacity);if(t instanceof wt||(t=Mt(t)),!t)return new Pt;if(t instanceof Pt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&c<1?0:u,new Pt(u,a,c,t.opacity)}function zt(t,n,e,r){return 1===arguments.length?Ct(t):new Pt(t,n,e,null==r?1:r)}function Pt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function qt(t){if(t instanceof Ut)return new Ut(t.l,t.a,t.b,t.opacity);if(t instanceof jt){var n=t.h*Ul;return new Ut(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof Et||(t=kt(t));var e=It(t.r),r=It(t.g),i=It(t.b),o=Dt((.4124564*e+.3575761*r+.1804375*i)/Fl),u=Dt((.2126729*e+.7151522*r+.072175*i)/Il),a=Dt((.0193339*e+.119192*r+.9503041*i)/Yl);return new Ut(116*u-16,500*(o-u),200*(u-a),t.opacity)}function Lt(t,n,e,r){return 1===arguments.length?qt(t):new Ut(t,n,e,null==r?1:r)}function Ut(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Dt(t){return t>Xl?Math.pow(t,1/3):t/Hl+Bl}function Ot(t){return t>jl?t*t*t:Hl*(t-Bl)}function Ft(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function It(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Yt(t){if(t instanceof jt)return new jt(t.h,t.c,t.l,t.opacity);t instanceof Ut||(t=qt(t));var n=Math.atan2(t.b,t.a)*Dl;return new jt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Bt(t,n,e,r){return 1===arguments.length?Yt(t):new jt(t,n,e,null==r?1:r)}function jt(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Ht(t){if(t instanceof Vt)return new Vt(t.h,t.s,t.l,t.opacity);t instanceof Et||(t=kt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(Kl*r+Jl*n-Ql*e)/(Kl+Jl-Ql),o=r-i,u=(Gl*(e-i)-$l*o)/Zl,a=Math.sqrt(u*u+o*o)/(Gl*i*(1-i)),c=a?Math.atan2(u,o)*Dl-120:NaN;return new Vt(c<0?c+360:c,a,i,t.opacity)}function Xt(t,n,e,r){return 1===arguments.length?Ht(t):new Vt(t,n,e,null==r?1:r)}function Vt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function $t(t,n){return function(e){return t+e*n}}function Zt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Gt(t,n){var e=n-t;return e?$t(t,e>180||e<-180?e-360*Math.round(e/360):e):ch(isNaN(t)?n:t)}function Jt(t){return 1===(t=+t)?Qt:function(n,e){return e-n?Zt(n,e,t):ch(isNaN(n)?e:n)}}function Qt(t,n){var e=n-t;return e?$t(t,e):ch(isNaN(t)?n:t)}function Kt(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:dh(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:dh(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:dh(t,e)},{i:a-2,x:dh(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e=0&&n._call.call(null,t),n=n._next;--Oh}function gn(){jh=(Bh=Xh.now())+Hh,Oh=Fh=0;try{yn()}finally{Oh=0,xn(),jh=0}}function mn(){var t=Xh.now(),n=t-Bh;n>Yh&&(Hh-=n,Bh=t)}function xn(){for(var t,n,e=ih,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:ih=n);oh=t,bn(r)}function bn(t){if(!Oh){Fh&&(Fh=clearTimeout(Fh));var n=t-jh;n>24?(t<1/0&&(Fh=setTimeout(gn,n)),Ih&&(Ih=clearInterval(Ih))):(Ih||(Bh=jh,Ih=setInterval(mn,Yh)),Oh=1,Vh(gn))}}function wn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Jh)throw new Error("too late");return e}function Mn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Kh)throw new Error("too late");return e}function Tn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function Nn(t,n,e){function r(t){e.state=Qh,e.timer.restart(i,e.delay,e.time),e.delay<=t&&i(t-e.delay)}function i(r){var s,f,l,h;if(e.state!==Qh)return u();for(s in c)if(h=c[s],h.name===e.name){if(h.state===tp)return Wh(i);h.state===np?(h.state=rp,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete c[s]):+s=0&&(t=t.slice(0,n)),!t||"start"===t})}function jn(t,n,e){var r,i,o=Bn(n)?wn:Mn;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function Hn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Xn(t,n){var e,r,i;return function(){var o=Kf(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function Vn(t){return function(){this.style.removeProperty(t)}}function Wn(t,n,e){var r,i;return function(){var o=Kf(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function $n(t,n,e){var r,i,o;return function(){var u=Kf(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Zn(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function Gn(t){return function(){this.textContent=t}}function Jn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Qn(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function Kn(t){return dt().transition(t)}function te(){return++kp}function ne(t){return+t}function ee(t){return t*t}function re(t){return t*(2-t)}function ie(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function oe(t){return t*t*t}function ue(t){return--t*t*t+1}function ae(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function ce(t){return 1-Math.cos(t*Rp)}function se(t){return Math.sin(t*Rp)}function fe(t){return(1-Math.cos(Pp*t))/2}function le(t){return Math.pow(2,10*t-10)}function he(t){return 1-Math.pow(2,-10*t)}function pe(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function de(t){return 1-Math.sqrt(1-t*t)}function ve(t){return Math.sqrt(1- --t*t)}function _e(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ye(t){return 1-ge(1-t)}function ge(t){return(t=+t)Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,ud(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],k){case cd:case ad:S&&(m=Math.max(P-l,Math.min(q-v,m)),h=l+m,_=v+m),E&&(x=Math.max(R-p,Math.min(L-y,x)),d=p+x,g=y+x);break;case sd:S<0?(m=Math.max(P-l,Math.min(q-l,m)),h=l+m,_=v):S>0&&(m=Math.max(P-v,Math.min(q-v,m)),h=l,_=v+m),E<0?(x=Math.max(R-p,Math.min(L-p,x)),d=p+x,g=y):E>0&&(x=Math.max(R-y,Math.min(L-y,x)),d=p,g=y+x);break;case fd:S&&(h=Math.max(P,Math.min(q,l-m*S)),_=Math.max(P,Math.min(q,v+m*S))),E&&(d=Math.max(R,Math.min(L,p-x*E)),g=Math.max(R,Math.min(L,y+x*E)))}_0&&(l=h-m),E<0?y=g-x:E>0&&(p=d-x),k=cd,Y.attr("cursor",dd.selection),o());break;default:return}ud()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:k===fd&&(S<0?v=_:S>0&&(l=h),E<0?y=g:E>0&&(p=d),k=sd,o());break;case 32:k===cd&&(t.event.altKey?(S&&(v=_-m*S,l=h+m*S),E&&(y=g-x*E,p=d+x*E),k=fd):(S<0?v=_:S>0&&(l=h),E<0?y=g:E>0&&(p=d),k=sd),Y.attr("cursor",dd[N]),o());break;default:return}ud()}if(t.event.touches){if(t.event.changedTouches.length=(o=(v+y)/2))?v=o:y=o,(f=e>=(u=(_+g)/2))?_=u:g=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+y)/2))?v=o:y=o,(f=e>=(u=(_+g)/2))?_=u:g=u;while((l=f<<1|s)===(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function Je(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;ef&&(f=r),il&&(l=i));for(f",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Av[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function lr(t){return t}function hr(n){return Pv=qv(n), +t.format=Pv.format,t.formatPrefix=Pv.formatPrefix,Pv}function pr(){this.reset()}function dr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function vr(t){return t>1?0:t<-1?m_:Math.acos(t)}function _r(t){return t>1?x_:t<-1?-x_:Math.asin(t)}function yr(t){return(t=R_(t/2))*t}function gr(){}function mr(t,n){t&&O_.hasOwnProperty(t.type)&&O_[t.type](t,n)}function xr(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=E_(n),u=R_(n),a=jv*u,c=Bv*o+a*E_(i),s=a*r*R_(i);I_.add(S_(s,c)),Yv=t,Bv=o,jv=u}function kr(t){return[S_(t[1],t[0]),_r(t[2])]}function Sr(t){var n=t[0],e=t[1],r=E_(e);return[r*E_(n),r*R_(n),R_(e)]}function Er(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Ar(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Cr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function zr(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Pr(t){var n=L_(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Rr(t,n){Qv.push(Kv=[Hv=t,Vv=t]),nWv&&(Wv=n)}function qr(t,n){var e=Sr([t*T_,n*T_]);if(Jv){var r=Ar(Jv,e),i=[r[1],-r[0],0],o=Ar(i,r);Pr(o),o=kr(o);var u,a=t-$v,c=a>0?1:-1,s=o[0]*M_*c,f=N_(a)>180;f^(c*$vWv&&(Wv=u)):(s=(s+360)%360-180,f^(c*$vWv&&(Wv=n))),f?t<$v?Ir(Hv,t)>Ir(Hv,Vv)&&(Vv=t):Ir(t,Vv)>Ir(Hv,Vv)&&(Hv=t):Vv>=Hv?(tVv&&(Vv=t)):t>$v?Ir(Hv,t)>Ir(Hv,Vv)&&(Vv=t):Ir(t,Vv)>Ir(Hv,Vv)&&(Hv=t)}else Rr(t,n);Jv=e,$v=t}function Lr(){X_.point=qr}function Ur(){Kv[0]=Hv,Kv[1]=Vv,X_.point=Rr,Jv=null}function Dr(t,n){if(Jv){var e=t-$v;H_.add(N_(e)>180?e+(e>0?360:-360):e)}else Zv=t,Gv=n;B_.point(t,n),qr(t,n)}function Or(){B_.lineStart()}function Fr(){Dr(Zv,Gv),B_.lineEnd(),N_(H_)>y_&&(Hv=-(Vv=180)),Kv[0]=Hv,Kv[1]=Vv,Jv=null}function Ir(t,n){return(n-=t)<0?n+360:n}function Yr(t,n){return t[0]-n[0]}function Br(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nm_?t-w_:t<-m_?t+w_:t,n]}function ti(t,n,e){return(t%=w_)?n||e?G_(ei(t),ri(n,e)):ei(t):n||e?ri(n,e):Kr}function ni(t){return function(n,e){return n+=t,[n>m_?n-w_:n<-m_?n+w_:n,e]}}function ei(t){var n=ni(t);return n.invert=ni(-t),n}function ri(t,n){function e(t,n){var e=E_(n),a=E_(t)*e,c=R_(t)*e,s=R_(n),f=s*r+a*i;return[S_(c*o-f*u,a*r-s*i),_r(f*o+c*u)]}var r=E_(t),i=R_(t),o=E_(n),u=R_(n);return e.invert=function(t,n){var e=E_(n),a=E_(t)*e,c=R_(t)*e,s=R_(n),f=s*o-c*u;return[S_(c*o+s*u,a*r+f*i),_r(f*r-a*i)]},e}function ii(t,n,e,r,i,o){if(e){var u=E_(n),a=R_(n),c=r*e;null==i?(i=n+r*w_,o=n-c/2):(i=oi(u,i),o=oi(u,o),(r>0?io)&&(i+=r*w_));for(var s,f=i;r>0?f>o:f0){do s.point(0===f||3===f?t:e,f>1?r:n);while((f=(f+a+4)%4)!==l)}else s.point(o[0],o[1])}function u(r,i){return N_(r[0]-t)0?0:3:N_(r[0]-e)0?2:1:N_(r[1]-n)0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&k.point(t,n)}function s(){for(var n=0,e=0,i=_.length;er&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){k=S,v=[],_=[],N=!0}function l(){var t=s(),n=N&&t,e=(v=Ks(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&py(v,a,t,o,u),u.polygonEnd()),k=u,v=_=y=null}function h(){E.point=d,_&&_.push(y=[]),T=!0,M=!1,b=w=NaN}function p(){v&&(d(g,m),x&&M&&S.rejoin(),v.push(S.result())),E.point=c,M&&k.lineEnd()}function d(o,u){var a=i(o,u);if(_&&y.push([o,u]),T)g=o,m=u,x=a,T=!1,a&&(k.lineStart(),k.point(o,u));else if(a&&M)k.point(o,u);else{var c=[b=Math.max(vy,Math.min(dy,b)),w=Math.max(vy,Math.min(dy,w))],s=[o=Math.max(vy,Math.min(dy,o)),u=Math.max(vy,Math.min(dy,u))];ly(c,s,t,n,e,r)?(M||(k.lineStart(),k.point(c[0],c[1])),k.point(s[0],s[1]),a||k.lineEnd(),N=!1):a&&(k.lineStart(),k.point(o,u),N=!1)}b=o,w=u,M=a}var v,_,y,g,m,x,b,w,M,T,N,k=u,S=fy(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function si(){gy.point=li,gy.lineEnd=fi}function fi(){gy.point=gy.lineEnd=gr}function li(t,n){t*=T_,n*=T_,J_=t,Q_=R_(n),K_=E_(n),gy.point=hi}function hi(t,n){t*=T_,n*=T_;var e=R_(n),r=E_(n),i=N_(t-J_),o=E_(i),u=R_(i),a=r*u,c=K_*e-Q_*r*o,s=Q_*e+K_*r*o;yy.add(S_(L_(a*a+c*c),s)),J_=t,Q_=e,K_=r}function pi(t,n,e){var r=Is(t,n-y_,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function di(t,n,e){var r=Is(t,n-y_,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function vi(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return Is(A_(o/_)*_,i,_).map(h).concat(Is(A_(s/y)*y,c,y).map(p)).concat(Is(A_(r/d)*d,e,d).filter(function(t){return N_(t%_)>y_}).map(f)).concat(Is(A_(a/v)*v,u,v).filter(function(t){return N_(t%y)>y_}).map(l))}var e,r,i,o,u,a,c,s,f,l,h,p,d=10,v=d,_=90,y=360,g=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[h(o).concat(p(c).slice(1),h(i).reverse().slice(1),p(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(g)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(g)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(_=+n[0],y=+n[1],t):[_,y]},t.stepMinor=function(n){return arguments.length?(d=+n[0],v=+n[1],t):[d,v]},t.precision=function(n){return arguments.length?(g=+n,f=pi(a,u,90),l=di(r,e,g),h=pi(s,c,90),p=di(o,i,g),t):g},t.extentMajor([[-180,-90+y_],[180,90-y_]]).extentMinor([[-180,-80-y_],[180,80+y_]])}function _i(){return vi()()}function yi(){Sy.point=gi}function gi(t,n){Sy.point=mi,ty=ey=t,ny=ry=n}function mi(t,n){ky.add(ry*t-ey*n),ey=t,ry=n}function xi(){mi(ty,ny)}function bi(t,n){tCy&&(Cy=t),nzy&&(zy=n)}function wi(t,n){Ry+=t,qy+=n,++Ly}function Mi(){By.point=Ti}function Ti(t,n){By.point=Ni,wi(uy=t,ay=n)}function Ni(t,n){var e=t-uy,r=n-ay,i=L_(e*e+r*r);Uy+=i*(uy+t)/2,Dy+=i*(ay+n)/2,Oy+=i,wi(uy=t,ay=n)}function ki(){By.point=wi}function Si(){By.point=Ai}function Ei(){Ci(iy,oy)}function Ai(t,n){By.point=Ci,wi(iy=uy=t,oy=ay=n)}function Ci(t,n){var e=t-uy,r=n-ay,i=L_(e*e+r*r);Uy+=i*(uy+t)/2,Dy+=i*(ay+n)/2,Oy+=i,i=ay*t-uy*n,Fy+=i*(uy+t),Iy+=i*(ay+n),Yy+=3*i,wi(uy=t,ay=n)}function zi(t){this._context=t}function Pi(){this._string=[]}function Ri(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function qi(t){return t.length>1}function Li(t,n){return((t=t.x)[0]<0?t[1]-x_-y_:x_-t[1])-((n=n.x)[0]<0?n[1]-x_-y_:x_-n[1])}function Ui(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?m_:-m_,c=N_(o-e);N_(c-m_)0?x_:-x_),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=m_&&(N_(e-i)y_?k_((R_(n)*(o=E_(r))*R_(e)-R_(r)*(i=E_(n))*R_(t))/(i*o*u)):(n+r)/2}function Oi(t,n,e,r){var i;if(null==t)i=e*x_,r.point(-m_,i),r.point(0,i),r.point(m_,i),r.point(m_,0),r.point(m_,-i),r.point(0,-i),r.point(-m_,-i),r.point(-m_,0),r.point(-m_,i);else if(N_(t[0]-n[0])>y_){var o=t[0]4*n&&v--){var x=u+h,b=a+p,w=c+d,M=L_(x*x+b*b+w*w),T=_r(w/=M),N=N_(N_(w)-1)n||N_((y*A+g*C)/m-.5)>.3||u*h+a*p+c*d2?t[2]%360*T_:0,i()):[b*M_,w*M_,M*M_]},n.precision=function(t){return arguments.length?(A=Qy(r,E=t*t),o()):L_(E)},n.fitExtent=function(t,e){return Yi(n,t,e)},n.fitSize=function(t,e){return Bi(n,t,e)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function Wi(t){var n=0,e=m_/3,r=Vi(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*T_,e=t[1]*T_):[n*M_,e*M_]},i}function $i(t){function n(t,n){return[t*e,R_(n)/e]}var e=E_(t);return n.invert=function(t,n){return[t/e,_r(n*e)]},n}function Zi(t,n){function e(t,n){var e=L_(o-2*i*R_(n))/i;return[e*R_(t*=i),u-e*E_(t)]}var r=R_(t),i=(r+R_(n))/2;if(N_(i)0?n<-x_+y_&&(n=-x_+y_):n>x_-y_&&(n=x_-y_);var e=o/P_(no(n),i);return[e*R_(i*t),o-e*E_(i*t)]}var r=E_(t),i=t===n?R_(t):z_(r/E_(n))/z_(no(n)/no(t)),o=r*P_(no(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=q_(i)*L_(t*t+e*e);return[S_(t,N_(e))/i*q_(e),2*k_(P_(o/r,1/i))-x_]},e):Ki}function ro(t,n){return[t,n]}function io(t,n){function e(t,n){var e=o-n,r=i*t;return[e*R_(r),o-e*E_(r)]}var r=E_(t),i=t===n?R_(t):(r-E_(n))/(n-t),o=r/i+t;return N_(i)=0;)n+=e[r].value;else n=1;t.value=n}function mo(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function xo(t,n){var e,r,i,o,u,a=new No(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=wo);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new No(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(To)}function bo(){return xo(this).eachBefore(Mo)}function wo(t){return t.children}function Mo(t){t.data=t.data.data}function To(t){var n=0;do t.height=n;while((t=t.parent)&&t.height<++n)}function No(t){this.data=t,this.depth=this.height=0,this.parent=null}function ko(t){this._=t,this.next=null}function So(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Eo(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=Ao(n[0]);break;case 2:e=Co(n[0],n[1]);break;case 3:e=zo(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&So(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Eo(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function Ao(t){return{x:t.x,y:t.y,r:t.r}}function Co(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function zo(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),y=2*(i-f),g=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*y,b=(p*m-y*v)/x-r,w=(y*d-p*g)/x,M=(_*v-h*m)/x-i,T=(h*g-_*d)/x,N=w*w+T*T-1,k=2*(b*w+M*T+o),S=b*b+M*M-o*o,E=(-k-Math.sqrt(k*k-4*N*S))/(2*N);return{x:b+w*E+r,y:M+T*E+i,r:E}}function Po(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Ro(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i-1e-6>e*e+r*r}function qo(t,n){for(var e=t._.r;t!==n;)e+=2*(t=t.next)._.r;return e-n._.r}function Lo(t,n,e){var r=t.x-n,i=t.y-e;return r*r+i*i}function Uo(t){this._=t,this.next=null,this.previous=null}function Do(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Po(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,y=h*n.y+p*e.y+d*r.y;n=new Uo(n),e=new Uo(e),r=new Uo(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;aqo(c,e)?n=c:e=c,n.next=e,e.previous=n,--a;continue t}f+=c._.r,c=c.next}else{if(Ro(s._,r._)){qo(n,s)>l+n._.r+e._.r?n=s:e=s,n.next=e,e.previous=n,--a;continue t}l+=s._.r,s=s.previous}while(c!==s.next);for(r.previous=n,r.next=e,n.next=e.previous=e=r,v+=d=r._.r*r._.r,_+=d*r._.x,y+=d*r._.y,h=Lo(n._,o=_/v,u=y/v);(r=r.next)!==e;)(d=Lo(r._,o,u))=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function Qo(t,n,e){return t.a.parent===n.parent?t.a:e}function Ko(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function tu(t){for(var n,e,r,i,o,u=new Ko(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new Ko(r[i],i)),e.parent=n;return(u.parent=new Ko(null,0)).children=[u],u}function nu(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,y=[],g=n.children,m=0,x=0,b=g.length,w=n.value;mh&&(h=a),_=f*f*v,p=Math.max(h/_,_/l),p>d){f-=a;break}d=p}y.push(u={value:f,dice:c1&&Gg(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function iu(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function ou(t){if(!t._start)try{uu(t)}catch(n){if(t._tasks[t._ended+t._active-1])cu(t,n);else if(!t._data)throw n}}function uu(t){for(;t._start=t._waiting&&t._active=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,su(t)}function su(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function fu(t){return new iu(arguments.length?+t:1/0)}function lu(t){return function(n,e){t(null==n?e:null)}}function hu(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function pu(t,n){return function(e){return t(e.responseText,n)}}function du(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==xm)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=Ie(),r=[],i=xm;return t=null==t?[]:mm.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=Ie();for(var i,o,u=-1,a=t.length;++u=e?1:r(t)}}}function xu(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function bu(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o2?wu:bu,o=u=null,r}function r(n){return(o||(o=i(a,c,f?mu(t):t,s)))(+n)}var i,o,u,a=Mm,c=Mm,s=mh,f=!1;return r.invert=function(t){return(u||(u=i(c,a,gu,f?xu(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=gm.call(t,wm),e()):a.slice()},r.range=function(t){return arguments.length?(c=mm.call(t),e()):c.slice()},r.rangeRound=function(t){return c=mm.call(t),s=xh,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function Nu(t){var n=t.domain;return t.ticks=function(t){var e=n();return Hs(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Tm(n(),t,e)},t.nice=function(r){var i=n(),o=i.length-1,u=null==r?10:r,a=i[0],c=i[o],s=e(a,c,u);return s&&(s=e(Math.floor(a/s)*s,Math.ceil(c/s)*s,u),i[0]=Math.floor(a/s)*s,i[o]=Math.ceil(c/s)*s,n(i)),t},t}function ku(){var t=Tu(gu,dh);return t.copy=function(){return Mu(t,ku())},Nu(t)}function Su(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=gm.call(e,wm),t):n.slice()},t.copy=function(){return Su().domain(n)},Nu(t)}function Eu(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:bm(n)}function Au(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function Cu(t){return isFinite(t)?+("1e"+t):t<0?0:t}function zu(t){return 10===t?Cu:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Pu(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Ru(t){return function(n){return-t(-n)}}function qu(){function n(){return o=Pu(i),u=zu(i),r()[0]<0&&(o=Ru(o),u=Ru(u)),e}var e=Tu(Eu,Au).domain([1,10]),r=e.domain,i=10,o=Pu(10),u=zu(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c0){for(;hc)break;v.push(l)}}else for(;h=1;--f)if(l=s*f,!(lc)break;v.push(l)}}else v=Hs(h,p,Math.min(p-h,d)).map(u);return n?v.reverse():v},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i0?i[n-1]:e[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return Fu().domain([e,r]).range(u)},Nu(t)}function Iu(){function t(t){if(t<=t)return e[Es(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=mm.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=mm.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Iu().domain(n).range(e)},t}function Yu(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do u.push(new Date(+e));while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return km.setTime(+n),Sm.setTime(+r),t(km),t(Sm),Math.floor(e(km,Sm))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t===0}:function(n){return i.count(0,n)%t===0}):i:null}),i}function Bu(t){return Yu(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*zm)/qm})}function ju(t){return Yu(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/qm})}function Hu(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Xu(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Vu(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function Wu(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date(+e));++a=c)return-1;if(i=n.charCodeAt(u++),37===i){if(i=n.charAt(u++),o=B[i in Px?n.charAt(u++):i],!o||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=q.exec(n.slice(e));return r?(t.w=L[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=R[r[0].toLowerCase()],e+r[0].length):-1; +}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return S[t.getDay()]}function p(t){return k[t.getDay()]}function d(t){return A[t.getMonth()]}function v(t){return E[t.getMonth()]}function _(t){return N[+(t.getHours()>=12)]}function y(t){return S[t.getUTCDay()]}function g(t){return k[t.getUTCDay()]}function m(t){return A[t.getUTCMonth()]}function x(t){return E[t.getUTCMonth()]}function b(t){return N[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,N=t.periods,k=t.days,S=t.shortDays,E=t.months,A=t.shortMonths,C=Gu(N),z=Ju(N),P=Gu(k),R=Ju(k),q=Gu(S),L=Ju(S),U=Gu(E),D=Ju(E),O=Gu(A),F=Ju(A),I={a:h,A:p,b:d,B:v,c:null,d:ha,e:ha,H:pa,I:da,j:va,L:_a,m:ya,M:ga,p:_,S:ma,U:xa,w:ba,W:wa,x:null,X:null,y:Ma,Y:Ta,Z:Na,"%":Ia},Y={a:y,A:g,b:m,B:x,c:null,d:ka,e:ka,H:Sa,I:Ea,j:Aa,L:Ca,m:za,M:Pa,p:b,S:Ra,U:qa,w:La,W:Ua,x:null,X:null,y:Da,Y:Oa,Z:Fa,"%":Ia},B={a:o,A:u,b:a,B:c,c:s,d:oa,e:oa,H:aa,I:aa,j:ua,L:fa,m:ia,M:ca,p:i,S:sa,U:Ku,w:Qu,W:ta,x:f,X:l,y:ea,Y:na,Z:ra,"%":la};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Hu);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Xu);return n.toString=function(){return t},n}}}function $u(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function ra(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function ia(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function oa(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function ua(t,n,e){var r=Rx.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function aa(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function ca(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function sa(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function fa(t,n,e){var r=Rx.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function la(t,n,e){var r=qx.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function ha(t,n){return $u(t.getDate(),n,2)}function pa(t,n){return $u(t.getHours(),n,2)}function da(t,n){return $u(t.getHours()%12||12,n,2)}function va(t,n){return $u(1+Ym.count(ox(t),t),n,3)}function _a(t,n){return $u(t.getMilliseconds(),n,3)}function ya(t,n){return $u(t.getMonth()+1,n,2)}function ga(t,n){return $u(t.getMinutes(),n,2)}function ma(t,n){return $u(t.getSeconds(),n,2)}function xa(t,n){return $u(jm.count(ox(t),t),n,2)}function ba(t){return t.getDay()}function wa(t,n){return $u(Hm.count(ox(t),t),n,2)}function Ma(t,n){return $u(t.getFullYear()%100,n,2)}function Ta(t,n){return $u(t.getFullYear()%1e4,n,4)}function Na(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+$u(n/60|0,"0",2)+$u(n%60,"0",2)}function ka(t,n){return $u(t.getUTCDate(),n,2)}function Sa(t,n){return $u(t.getUTCHours(),n,2)}function Ea(t,n){return $u(t.getUTCHours()%12||12,n,2)}function Aa(t,n){return $u(1+lx.count(Ax(t),t),n,3)}function Ca(t,n){return $u(t.getUTCMilliseconds(),n,3)}function za(t,n){return $u(t.getUTCMonth()+1,n,2)}function Pa(t,n){return $u(t.getUTCMinutes(),n,2)}function Ra(t,n){return $u(t.getUTCSeconds(),n,2)}function qa(t,n){return $u(px.count(Ax(t),t),n,2)}function La(t){return t.getUTCDay()}function Ua(t,n){return $u(dx.count(Ax(t),t),n,2)}function Da(t,n){return $u(t.getUTCFullYear()%100,n,2)}function Oa(t,n){return $u(t.getUTCFullYear()%1e4,n,4)}function Fa(){return"+0000"}function Ia(){return"%"}function Ya(n){return Cx=Wu(n),t.timeFormat=Cx.format,t.timeParse=Cx.parse,t.utcFormat=Cx.utcFormat,t.utcParse=Cx.utcParse,Cx}function Ba(t){return t.toISOString()}function ja(t){var n=new Date(t);return isNaN(n)?null:n}function Ha(t){return new Date(t)}function Xa(t){return t instanceof Date?+t:+new Date(+t)}function Va(t,n,r,i,o,u,a,c,s){function f(e){return(a(e)=1?lb:t<=-1?-lb:Math.asin(t)}function nc(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function ec(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/Math.sqrt(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,y=(p+v)/2,g=d-h,m=v-p,x=g*g+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*Math.sqrt(Math.max(0,b*b*x-w*w)),T=(w*m-g*M)/x,N=(-w*g-m*M)/x,k=(w*m+g*M)/x,S=(-w*g+m*M)/x,E=T-_,A=N-y,C=k-_,z=S-y;return E*E+A*A>C*C+z*z&&(T=k,N=S),{cx:T,cy:N,x01:-f,y01:-l,x11:T*(i/b-1),y11:N*(i/b-1)}}function rc(t){this._context=t}function ic(t){return t[0]}function oc(t){return t[1]}function uc(t){this._curve=t}function ac(t){function n(n){return new uc(t(n))}return n._curve=t,n}function cc(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(ac(t)):n()._curve},t}function sc(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function fc(t){this._context=t}function lc(t){this._context=t}function hc(t){this._context=t}function pc(t,n){this._basis=new fc(t),this._beta=n}function dc(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function vc(t,n){this._context=t,this._k=(1-n)/6}function _c(t,n){this._context=t,this._k=(1-n)/6}function yc(t,n){this._context=t,this._k=(1-n)/6}function gc(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>sb){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>sb){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function mc(t,n){this._context=t,this._alpha=n}function xc(t,n){this._context=t,this._alpha=n}function bc(t,n){this._context=t,this._alpha=n}function wc(t){this._context=t}function Mc(t){return t<0?-1:1}function Tc(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Mc(o)+Mc(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Nc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function kc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function Sc(t){this._context=t}function Ec(t){this._context=new Ac(t)}function Ac(t){this._context=t}function Cc(t){return new Sc(t)}function zc(t){return new Ec(t)}function Pc(t){this._context=t}function Rc(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n0)){if(o/=d,d<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o0)){if(o/=v,v<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function Gc(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]ww||Math.abs(i[0][1]-i[1][1])>ww)||delete mw[o]}function Qc(t){return yw[t.index]={site:t,halfedges:[]}}function Kc(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function ts(t,n){return n[+(n.left!==t.site)]}function ns(t,n){return n[+(n.left===t.site)]}function es(){for(var t,n,e,r,i=0,o=yw.length;iww||Math.abs(v-h)>ww)&&(c.splice(a,0,mw.push(Wc(u,p,Math.abs(d-t)ww?[t,Math.abs(l-t)ww?[Math.abs(h-r)ww?[e,Math.abs(l-e)ww?[Math.abs(h-n)=-Mw)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,y=xw.pop()||new is;y.arc=t,y.site=i,y.x=v+u,y.y=(y.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=y;for(var g=null,m=gw._;m;)if(y.yww)a=a.L;else{if(i=o-ps(a,u),!(i>ww)){r>-ww?(n=a.P,e=a):i>-ww?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}Qc(t);var c=cs(t);if(_w.insert(n,c),n||e){if(n===e)return us(n),e=cs(n.site),_w.insert(c,e),c.edge=e.edge=Vc(n.site,c.site),os(n),void os(e);if(!e)return void(c.edge=Vc(n.site,c.site));us(n),us(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,y=2*(h*_-p*v),g=h*h+p*p,m=v*v+_*_,x=[(_*g-p*m)/y+f,(h*m-v*g)/y+l];$c(e.edge,s,d,x),c.edge=Vc(s,t,null,x),e.edge=Vc(t,d,null,x),os(n),os(e)}}function hs(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function ps(t,n){var e=t.N;if(e)return hs(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function ds(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function vs(t,n){return n[1]-t[1]||n[0]-t[0]}function _s(t,n){var e,r,i,o=t.sort(vs).pop();for(mw=[],yw=new Array(t.length),_w=new Yc,gw=new Yc;;)if(i=vw,o&&(!i||o[1]n?1:t>=n?0:NaN},ks=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}},Ss=ks(Ns),Es=Ss.right,As=Ss.left,Cs=function(t,n){return nt?1:n>=t?0:NaN},zs=function(t){return null===t?NaN:+t},Ps=function(t,n){var e,r,i=t.length,o=0,u=0,a=-1,c=0;if(null==n)for(;++a1)return u/(c-1)},Rs=function(t,n){var e=Ps(t,n);return e?Math.sqrt(e):e},qs=function(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o=r){e=i=r;break}for(;++or&&(e=r),i=r){e=i=r;break}for(;++or&&(e=r),i=f;)l.pop(),--h;var p,d=new Array(h+1);for(i=0;i<=h;++i)p=d[i]=[],p.x0=i>0?l[i-1]:s,p.x1=i=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),u=+e(t[o],o,t),a=+e(t[o+1],o+1,t);return u+(a-u)*(i-o)}},$s=function(t,n,e){return t=Ds.call(t,zs).sort(Ns),Math.ceil((e-n)/(2*(Ws(t,.75)-Ws(t,.25))*Math.pow(t.length,-1/3)))},Zs=function(t,n,e){return Math.ceil((e-n)/(3.5*Rs(t)*Math.pow(t.length,-1/3)))},Gs=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},Js=function(t,n){var e,r=0,i=t.length,o=-1,u=i;if(null==n)for(;++o=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e},tf=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},nf=function(t){for(var n=0,e=t.length-1,r=t[0],i=new Array(e<0?0:e);n0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),gf.hasOwnProperty(n)?{space:gf[n],local:t}:t},xf=function(t){var n=mf(t);return(n.local?m:g)(n)},bf=0;b.prototype=x.prototype={constructor:b,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var wf=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Mf=document.documentElement;if(!Mf.matches){var Tf=Mf.webkitMatchesSelector||Mf.msMatchesSelector||Mf.mozMatchesSelector||Mf.oMatchesSelector;wf=function(t){return function(){return Tf.call(this,t)}}}}var Nf=wf,kf={};if(t.event=null,"undefined"!=typeof document){var Sf=document.documentElement;"onmouseenter"in Sf||(kf={mouseenter:"mouseover",mouseleave:"mouseout"})}var Ef=function(t,n,e){var r,i,o=T(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?k:N,null==e&&(e=!1),r=0;r=b&&(b=x+1);!(m=_[b])&&++b=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Xf=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=R);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?I:"function"==typeof n?B:Y)(t,n,null==e?"":e)):Kf(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},nl=function(t,n){return arguments.length>1?this.each((null==n?j:"function"==typeof n?X:H)(t,n)):this.node()[t]};$.prototype={add:function(t){var n=this._names.indexOf(t);n<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var el=function(t,n){var e=V(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=e.length;++i=240?t-240:t+120,i,r),Rt(t,i,r),Rt(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Ul=Math.PI/180,Dl=180/Math.PI,Ol=18,Fl=.95047,Il=1,Yl=1.08883,Bl=4/29,jl=6/29,Hl=3*jl*jl,Xl=jl*jl*jl;bl(Ut,Lt,bt(wt,{brighter:function(t){return new Ut(this.l+Ol*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Ut(this.l-Ol*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Il*Ot(t),n=Fl*Ot(n),e=Yl*Ot(e),new Et(Ft(3.2404542*n-1.5371385*t-.4985314*e),Ft(-.969266*n+1.8760108*t+.041556*e),Ft(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),bl(jt,Bt,bt(wt,{brighter:function(t){return new jt(this.h,this.c,this.l+Ol*(null==t?1:t),this.opacity)},darker:function(t){return new jt(this.h,this.c,this.l-Ol*(null==t?1:t),this.opacity)},rgb:function(){return qt(this).rgb()}}));var Vl=-.14861,Wl=1.78277,$l=-.29227,Zl=-.90649,Gl=1.97294,Jl=Gl*Zl,Ql=Gl*Wl,Kl=Wl*$l-Zl*Vl;bl(Vt,Xt,bt(wt,{brighter:function(t){return t=null==t?Ml:Math.pow(Ml,t),new Vt(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?wl:Math.pow(wl,t),new Vt(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Ul,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new Et(255*(n+e*(Vl*r+Wl*i)),255*(n+e*($l*r+Zl*i)),255*(n+e*(Gl*r)),this.opacity)}}));var th,nh,eh,rh,ih,oh,uh=function(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:dh(e,r)})),o=yh.lastIndex;return oKh&&e.stateQh&&e.name===n)return new Qn([[t]],ed,n,+r)}return null},id=function(t){return function(){return t}},od=function(t,n,e){this.target=t,this.type=n,this.selection=e},ud=function(){t.event.preventDefault(),t.event.stopImmediatePropagation()},ad={name:"drag"},cd={name:"space"},sd={name:"handle"},fd={name:"center"},ld={name:"x",handles:["e","w"].map(we),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},hd={name:"y",handles:["n","s"].map(we),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},pd={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(we),input:function(t){return t},output:function(t){return t}},dd={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},vd={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},_d={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},yd={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},gd={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},md=function(){return Ce(pd)},xd=Math.cos,bd=Math.sin,wd=Math.PI,Md=wd/2,Td=2*wd,Nd=Math.max,kd=function(){function t(t){var o,u,a,c,s,f,l=t.length,h=[],p=Is(l),d=[],v=[],_=v.groups=new Array(l),y=new Array(l*l);for(o=0,s=-1;++szd)if(Math.abs(f*a-c*s)>zd&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),y=Math.sqrt(l),g=i*Math.tan((Ad-Math.acos((d+l-v)/(2*_*y)))/2),m=g/y,x=g/_;Math.abs(m-1)>zd&&(this._+="L"+(t+m*s)+","+(n+m*f)),this._+="A"+i+","+i+",0,0,"+ +(f*h>s*p)+","+(this._x1=t+x*a)+","+(this._y1=n+x*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+s:(Math.abs(this._x1-c)>zd||Math.abs(this._y1-s)>zd)&&(this._+="L"+c+","+s),e&&(l>Pd?this._+="A"+e+","+e+",0,1,"+f+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+f+","+(this._x1=c)+","+(this._y1=s):(l<0&&(l=l%Cd+Cd),this._+="A"+e+","+e+",0,"+ +(l>=Ad)+","+f+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var Rd=function(){function t(){var t,a=Sd.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,a)),l=i.apply(this,a)-Md,h=o.apply(this,a)-Md,p=f*xd(l),d=f*bd(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-Md,y=o.apply(this,a)-Md;if(u||(u=t=Re()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===y||(u.quadraticCurveTo(0,0,v*xd(_),v*bd(_)),u.arc(0,0,v,_,y)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=qe,e=Le,r=Ue,i=De,o=Oe,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:Ed(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Ed(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Ed(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t},qd="$";Fe.prototype=Ie.prototype={constructor:Fe,has:function(t){return qd+t in this},get:function(t){return this[qd+t]},set:function(t,n){return this[qd+t]=n,this},remove:function(t){var n=qd+t;return n in this&&delete this[n]},clear:function(){for(var t in this)t[0]===qd&&delete this[t]},keys:function(){var t=[];for(var n in this)n[0]===qd&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)n[0]===qd&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)n[0]===qd&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)n[0]===qd&&++t;return t},empty:function(){for(var t in this)if(t[0]===qd)return!1;return!0},each:function(t){for(var n in this)n[0]===qd&&t(this[n],n.slice(1),this)}};var Ld=function(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=Ie(),v=u();++lo.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,Ye,Be)},map:function(n){return t(n,0,je,He)},entries:function(e){return n(t(e,0,je,He),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},Ud=Ie.prototype;Xe.prototype=Ve.prototype={constructor:Xe,has:Ud.has,add:function(t){return t+="",this[qd+t]=t,this},remove:Ud.remove,clear:Ud.clear,values:Ud.keys,size:Ud.size,empty:Ud.empty,each:Ud.each};var Dd=function(t){var n=[];for(var e in t)n.push(e);return n},Od=function(t){var n=[];for(var e in t)n.push(t[e]);return n},Fd=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},Id=function(t){function n(t,n){var r,i,o=e(t,function(t,e){return r?r(t,e-1):(i=t,void(r=n?$e(t,n):We(t)))});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do u=new Array(4),u[a]=s,s=u;while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do u=new Array(4),u[a]=s,s=u;while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do u=new Array(4),u[a]=s,s=u;while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do u=new Array(4),u[a]=s,s=u;while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},ev=function(){var t=[];return this.visit(function(n){if(!n.length)do t.push(n.data);while(n=n.next)}),t},rv=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},iv=function(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i},ov=function(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new iv(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)=y)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var g=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=g*g+m*m;if(x=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+y)/2))?v=c:y=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)},av=function(){return this._root},cv=function(){var t=0;return this.visit(function(n){if(!n.length)do++t;while(n=n.next)}),t},sv=function(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new iv(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new iv(e,s,f,o,u)),(e=c[2])&&a.push(new iv(e,r,f,s,u)),(e=c[1])&&a.push(new iv(e,s,i,o,f)),(e=c[0])&&a.push(new iv(e,r,i,s,f))}return this},fv=function(t){var n,e=[],r=[];for(this._root&&e.push(new iv(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new iv(o,u,a,f,l)),(o=i[1])&&e.push(new iv(o,f,a,c,l)),(o=i[2])&&e.push(new iv(o,u,l,f,s)),(o=i[3])&&e.push(new iv(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},lv=function(t){return arguments.length?(this._x=t,this):this._x},hv=function(t){return arguments.length?(this._y=t,this):this._y},pv=nr.prototype=er.prototype;pv.copy=function(){var t,n,e=new er(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=rr(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=rr(n));return e},pv.add=tv,pv.addAll=Je,pv.cover=nv,pv.data=ev,pv.extent=rv,pv.find=ov,pv.remove=uv,pv.removeAll=Qe,pv.root=av,pv.size=cv,pv.visit=sv,pv.visitAfter=fv,pv.x=lv,pv.y=hv;var dv,vv=function(t){function n(){function t(t,n,e,r,i){var o=t.data,a=t.r,p=l+a;{if(!o)return n>s+p||rf+p||ic.index){var d=s-o.x-o.vx,v=f-o.y-o.vy,_=d*d+v*v;_t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s1?(d.on(t,n),o):d.on(t)}}},xv=function(){function t(t){var n,a=i.length,c=nr(i,cr,sr).visitAfter(e);for(u=t,n=0;n=f)){(t.data!==o||t.next)&&(0===i&&(i=Kd(),p+=i*i),0===c&&(c=Kd(),p+=c*c),p1?r[0]+r.slice(2):r,+t.slice(e+1)]},Tv=function(t){return t=Mv(Math.abs(t)),t?t[1]:NaN},Nv=function(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},kv=function(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Sv=function(t,n){var e=Mv(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(dv=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Mv(t,Math.max(0,n+o-1))[0]},Ev=function(t,n){var e=Mv(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")},Av={"":kv,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Ev(100*t,n)},r:Ev,s:Sv,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Cv=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,zv=function(t){return new fr(t)};fr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Pv,Rv=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],qv=function(t){function n(t){function n(t){var n,i,c,g=d,m=v;if("c"===p)m=_(t)+m,t="";else{t=+t;var x=(t<0||1/t<0)&&(t*=-1,!0);if(t=_(t,h),x)for(n=-1,i=t.length,x=!1;++nc||c>57){m=(46===c?o+t.slice(n+1):t.slice(n))+m,t=t.slice(0,n);break}}l&&!s&&(t=r(t,1/0));var b=g.length+t.length+m.length,w=b>1)+g+t+m+w.slice(b)}return w+g+t+m}t=zv(t);var e=t.fill,u=t.align,a=t.sign,c=t.symbol,s=t.zero,f=t.width,l=t.comma,h=t.precision,p=t.type,d="$"===c?i[0]:"#"===c&&/[boxX]/.test(p)?"0"+p.toLowerCase():"",v="$"===c?i[1]:/[%p]/.test(p)?"%":"",_=Av[p],y=!p||/[defgprs%]/.test(p);return h=null==h?p?6:12:/[gprs]/.test(p)?Math.max(1,Math.min(21,h)):Math.max(0,Math.min(20,h)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=zv(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(Tv(e)/3))),o=Math.pow(10,-i),u=Rv[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?Nv(t.grouping,t.thousands):lr,i=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};hr({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Lv=function(t){return Math.max(0,-Tv(Math.abs(t)))},Uv=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Tv(n)/3)))-Tv(Math.abs(t)))},Dv=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Tv(n)-Tv(t))+1},Ov=function(){return new pr};pr.prototype={constructor:pr,reset:function(){this.s=this.t=0},add:function(t){dr(__,t,this.t),dr(this,__.s,this.s),this.s?this.t+=__.t:this.s=__.t},valueOf:function(){return this.s}};var Fv,Iv,Yv,Bv,jv,Hv,Xv,Vv,Wv,$v,Zv,Gv,Jv,Qv,Kv,t_,n_,e_,r_,i_,o_,u_,a_,c_,s_,f_,l_,h_,p_,d_,v_,__=new pr,y_=1e-6,g_=1e-12,m_=Math.PI,x_=m_/2,b_=m_/4,w_=2*m_,M_=180/m_,T_=m_/180,N_=Math.abs,k_=Math.atan,S_=Math.atan2,E_=Math.cos,A_=Math.ceil,C_=Math.exp,z_=Math.log,P_=Math.pow,R_=Math.sin,q_=Math.sign||function(t){return t>0?1:t<0?-1:0},L_=Math.sqrt,U_=Math.tan,D_={Feature:function(t,n){mr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++ry_?Wv=90:H_<-y_&&(Xv=-90),Kv[0]=Hv,Kv[1]=Vv}},V_=function(t){var n,e,r,i,o,u,a;if(Wv=Vv=-(Hv=Xv=1/0),Qv=[],F_(t,X_),e=Qv.length){for(Qv.sort(Yr),n=1,r=Qv[0],o=[r];nIr(r[0],r[1])&&(r[1]=i[1]),Ir(i[0],r[1])>Ir(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Ir(r[1],i[0]))>u&&(u=a,Hv=i[0],Vv=r[1])}return Qv=Kv=null,Hv===1/0||Xv===1/0?[[NaN,NaN],[NaN,NaN]]:[[Hv,Xv],[Vv,Wv]]},W_={sphere:gr,point:jr,lineStart:Xr,lineEnd:$r,polygonStart:function(){W_.lineStart=Zr,W_.lineEnd=Gr},polygonEnd:function(){W_.lineStart=Xr,W_.lineEnd=$r}},$_=function(t){t_=n_=e_=r_=i_=o_=u_=a_=c_=s_=f_=0,F_(t,W_);var n=c_,e=s_,r=f_,i=n*n+e*e+r*r;return i2?t[2]*T_:0),n.invert=function(n){return n=t.invert(n[0]*T_,n[1]*T_),n[0]*=M_,n[1]*=M_,n},n},sy=function(){function t(t,n){e.push(t=r(t,n)),t[0]*=M_,t[1]*=M_}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*T_,c=u.apply(this,arguments)*T_;return e=[],r=ti(-t[0]*T_,-t[1]*T_,0).invert,ii(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=Z_([0,0]),o=Z_(90),u=Z_(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:Z_([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:Z_(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:Z_(+t),n):u},n},fy=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:gr,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},ly=function(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u0)){if(u/=d,d<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}},hy=function(t,n){return N_(t[0]-n[0])=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}},dy=1e9,vy=-dy,_y=function(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=ci(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}},yy=Ov(),gy={sphere:gr,point:gr,lineStart:si,lineEnd:gr,polygonStart:gr,polygonEnd:gr},my=function(t){return yy.reset(),F_(t,gy),+yy},xy=[null,null],by={type:"LineString",coordinates:xy},wy=function(t,n){return xy[0]=t,xy[1]=n,my(by)},My=function(t,n){var e=t[0]*T_,r=t[1]*T_,i=n[0]*T_,o=n[1]*T_,u=E_(r),a=R_(r),c=E_(o),s=R_(o),f=u*E_(e),l=u*R_(e),h=c*E_(i),p=c*R_(i),d=2*_r(L_(yr(o-r)+u*c*yr(i-e))),v=R_(d),_=d?function(t){var n=R_(t*=d)/v,e=R_(d-t)/v,r=e*f+n*h,i=e*l+n*p,o=e*a+n*s;return[S_(i,r)*M_,S_(o,L_(r*r+i*i))*M_]}:function(){return[e*M_,r*M_]};return _.distance=d,_},Ty=function(t){return t},Ny=Ov(),ky=Ov(),Sy={point:gr,lineStart:gr,lineEnd:gr,polygonStart:function(){Sy.lineStart=yi,Sy.lineEnd=xi},polygonEnd:function(){Sy.lineStart=Sy.lineEnd=Sy.point=gr,Ny.add(N_(ky)),ky.reset()},result:function(){var t=Ny/2;return Ny.reset(),t}},Ey=1/0,Ay=Ey,Cy=-Ey,zy=Cy,Py={point:bi,lineStart:gr,lineEnd:gr,polygonStart:gr,polygonEnd:gr,result:function(){var t=[[Ey,Ay],[Cy,zy]];return Cy=zy=-(Ay=Ey=1/0),t}},Ry=0,qy=0,Ly=0,Uy=0,Dy=0,Oy=0,Fy=0,Iy=0,Yy=0,By={point:wi,lineStart:Mi,lineEnd:ki,polygonStart:function(){By.lineStart=Si,By.lineEnd=Ei},polygonEnd:function(){By.point=wi,By.lineStart=Mi,By.lineEnd=ki},result:function(){var t=Yy?[Fy/Yy,Iy/Yy]:Oy?[Uy/Oy,Dy/Oy]:Ly?[Ry/Ly,qy/Ly]:[NaN,NaN];return Ry=qy=Ly=Uy=Dy=Oy=Fy=Iy=Yy=0,t}};zi.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,w_)}},result:gr},Pi.prototype={_circle:Ri(4.5),pointRadius:function(t){return this._circle=Ri(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var jy=function(t,n){function e(t){return t&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),F_(t,r(i))),i.result()}var r,i,o=4.5;return e.area=function(t){return F_(t,r(Sy)),Sy.result()},e.bounds=function(t){return F_(t,r(Py)),Py.result()},e.centroid=function(t){return F_(t,r(By)),By.result()},e.projection=function(n){return arguments.length?(r=null==n?(t=null,Ty):(t=n).stream,e):t},e.context=function(t){return arguments.length?(i=null==t?(n=null,new Pi):new zi(n=t),"function"!=typeof o&&i.pointRadius(o),e):n},e.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(i.pointRadius(+t),+t),e):o},e.projection(t).context(n)},Hy=Ov(),Xy=function(t,n){var e=n[0],r=n[1],i=[R_(e),-E_(e),0],o=0,u=0;Hy.reset();for(var a=0,c=t.length;a=0?1:-1,T=M*w,N=T>m_,k=d*x;if(Hy.add(S_(k*M*R_(T),v*b+k*E_(T))),o+=N?w+M*w_:w,N^h>=e^g>=e){var S=Ar(Sr(l),Sr(y));Pr(S);var E=Ar(i,S);Pr(E);var A=(N^w>=0?-1:1)*_r(E[2]);(r>A||r===A&&(S[0]||S[1]))&&(u+=N^w>=0?1:-1)}}return(o<-y_||o0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(qi))}var p,d,v,_=n(o),y=i.invert(r[0],r[1]),g=fy(),m=n(g),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=Ks(d);var t=Xy(p,y);d.length?(x||(o.polygonStart(),x=!0),py(d,Li,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}},Wy=Vy(function(){return!0},Ui,Oi,[-m_,-x_]),$y=function(t,n){function e(e,r,i,o){ii(o,t,n,i,e,r)}function r(t,n){return E_(t)*E_(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?m_:-m_),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(hy(n,p)||hy(d,p))&&(d[0]+=y_,d[1]+=y_,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var y;_&e||!(y=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&hy(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=Sr(t),i=Sr(n),o=[1,0,0],u=Ar(r,i),c=Er(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=Ar(o,u),d=zr(o,l),v=zr(u,h);Cr(d,v);var _=p,y=Er(d,_),g=Er(_,_),m=y*y-g*(Er(d,d)-1);if(!(m<0)){var x=L_(m),b=zr(_,(-y-x)/g);if(Cr(b,d),b=kr(b),!e)return b;var w,M=t[0],T=n[0],N=t[1],k=n[1];T0^b[1]<(N_(b[0]-M)m_^(M<=b[0]&&b[0]<=T)){var C=zr(_,(-y+x)/g);return Cr(C,d),[b,kr(C)]}}}function u(n,e){var r=c?t:m_-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=E_(t),c=a>0,s=N_(a)>y_;return Vy(r,i,e,c?[0,-t]:[-m_,t-m_])},Zy=function(t){return{stream:Fi(t)}};Ii.prototype={constructor:Ii,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Gy=16,Jy=E_(30*T_),Qy=function(t,n){return+n?Hi(t,n):ji(t)},Ky=Fi({point:function(t,n){this.stream.point(t*T_,n*T_)}}),tg=function(){return Wi(Zi).scale(155.424).center([0,33.6442])},ng=function(){return tg().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])},eg=function(){function t(t){var n=t[0],e=t[1];return a=null,i.point(n,e),a||(o.point(n,e),a)||(u.point(n,e),a)}function n(){return e=r=null,t}var e,r,i,o,u,a,c=ng(),s=tg().rotate([154,0]).center([-2,58.5]).parallels([55,65]),f=tg().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,n){a=[t,n]}};return t.invert=function(t){var n=c.scale(),e=c.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?f:c).invert(t)},t.stream=function(t){return e&&r===t?e:e=Gi([c.stream(r=t),s.stream(t),f.stream(t)])},t.precision=function(t){return arguments.length?(c.precision(t),s.precision(t),f.precision(t),n()):c.precision()},t.scale=function(n){return arguments.length?(c.scale(n),s.scale(.35*n),f.scale(n),t.translate(c.translate())):c.scale()},t.translate=function(t){if(!arguments.length)return c.translate();var e=c.scale(),r=+t[0],a=+t[1];return i=c.translate(t).clipExtent([[r-.455*e,a-.238*e],[r+.455*e,a+.238*e]]).stream(l),o=s.translate([r-.307*e,a+.201*e]).clipExtent([[r-.425*e+y_,a+.12*e+y_],[r-.214*e-y_,a+.234*e-y_]]).stream(l),u=f.translate([r-.205*e,a+.212*e]).clipExtent([[r-.214*e+y_,a+.166*e+y_],[r-.115*e-y_,a+.234*e-y_]]).stream(l),n()},t.fitExtent=function(n,e){return Yi(t,n,e)},t.fitSize=function(n,e){return Bi(t,n,e)},t.scale(1070)},rg=Ji(function(t){return L_(2/(1+t))});rg.invert=Qi(function(t){return 2*_r(t/2)});var ig=function(){return Xi(rg).scale(124.75).clipAngle(179.999)},og=Ji(function(t){return(t=vr(t))&&t/R_(t)});og.invert=Qi(function(t){return t});var ug=function(){return Xi(og).scale(79.4188).clipAngle(179.999)};Ki.invert=function(t,n){return[t,2*k_(C_(n))-x_]};var ag=function(){return to(Ki).scale(961/w_)},cg=function(){return Wi(eo).scale(109.5).parallels([30,30])};ro.invert=ro;var sg=function(){return Xi(ro).scale(152.63)},fg=function(){return Wi(io).scale(131.154).center([0,13.9389])};oo.invert=Qi(k_);var lg=function(){return Xi(oo).scale(144.049).clipAngle(60)},hg=function(){function t(){return i=o=null,u}var n,e,r,i,o,u,a=1,c=0,s=0,f=1,l=1,h=Ty,p=null,d=Ty;return u={stream:function(t){return i&&o===t?i:i=h(d(o=t))},clipExtent:function(i){return arguments.length?(d=null==i?(p=n=e=r=null,Ty):ci(p=+i[0][0],n=+i[0][1],e=+i[1][0],r=+i[1][1]),t()):null==p?null:[[p,n],[e,r]]},scale:function(n){return arguments.length?(h=uo((a=+n)*f,a*l,c,s),t()):a},translate:function(n){return arguments.length?(h=uo(a*f,a*l,c=+n[0],s=+n[1]),t()):[c,s]},reflectX:function(n){return arguments.length?(h=uo(a*(f=n?-1:1),a*l,c,s),t()):f<0},reflectY:function(n){return arguments.length?(h=uo(a*f,a*(l=n?-1:1),c,s),t()):l<0},fitExtent:function(t,n){return Yi(u,t,n)},fitSize:function(t,n){return Bi(u,t,n)}}};ao.invert=Qi(_r);var pg=function(){return Xi(ao).scale(249.5).clipAngle(90+y_)};co.invert=Qi(function(t){return 2*k_(t)});var dg=function(){return Xi(co).scale(250).clipAngle(142)};so.invert=function(t,n){return[-n,2*k_(C_(t))-x_]};var vg=function(){var t=to(so),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},_g=function(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=lo(e),t.y=po(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=_o(t),c=yo(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=fo,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},yg=function(){return this.eachAfter(go)},gg=function(t){var n,e,r,i,o=this,u=[o];do for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},xg=function(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e=0;)e+=r[i].value;n.value=e})},wg=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Mg=function(t){for(var n=this,e=mo(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},Tg=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Ng=function(){var t=[];return this.each(function(n){t.push(n)}),t},kg=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Sg=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};No.prototype=xo.prototype={constructor:No,count:yg,each:gg,eachAfter:xg,eachBefore:mg,sum:bg,sort:wg,path:Mg,ancestors:Tg,descendants:Ng,leaves:kg,links:Sg,copy:bo};var Eg=function(t){for(var n,e=(t=t.slice()).length,r=null,i=r;e;){var o=new ko(t[e-1]);i=i?i.next=o:r=o,t[n]=t[--e]}return{head:r,tail:i}},Ag=function(t){return Eo(Eg(t),[])},Cg=function(t){return Do(t),t},zg=function(t){return function(){return t}},Pg=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Bo(n)).eachAfter(jo(i,.5)).eachBefore(Ho(1)):t.eachBefore(Bo(Yo)).eachAfter(jo(Io,1)).eachAfter(jo(i,t.r/Math.min(e,r))).eachBefore(Ho(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Io;return t.radius=function(e){return arguments.length?(n=Oo(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:zg(+n),t):i},t},Rg=function(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)},qg=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a0)throw new Error("cycle");return o}var n=Xo,e=Vo;return t.id=function(e){return arguments.length?(n=Fo(e),t):n},t.parentId=function(n){return arguments.length?(e=Fo(n),t):e},t};Ko.prototype=Object.create(No.prototype);var Ig=function(){function t(t){var r=tu(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.xf.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){Jo(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=Zo(a),i=$o(i),a&&i;)c=$o(c),u=Zo(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(Go(Qo(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!Zo(u)&&(u.t=a,u.m+=l-f),i&&!$o(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=Wo,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t},Yg=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a1?n:1)},e}(Bg),Hg=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Rg),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p>>1;f[v]u-r){var g=(i*y+a*_)/e;o(t,p,_,r,i,u,g),o(p,n,y,r,g,u,a)}else{var m=(r*y+u*_)/e;o(t,p,_,r,i,m,a),o(p,n,y,m,i,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u1?n:1)},e}(Bg),$g=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;na!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f},Kg=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}},im=function(){var t=rm.apply(this,arguments);return function(){return Math.exp(t())}},om=function(t){ +return function(){for(var n=0,e=0;e=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(t){return void a.call("error",r,t)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=p("beforesend","progress","load","error"),c=Ie(),s=new XMLHttpRequest,f=null,l=null,h=0;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=lu(o)),null!=o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return r.get(n)}return r},sm=function(t,n){return function(e,r){var i=cm(e).mimeType(t).response(n);if(null!=r){if("function"!=typeof r)throw new Error("invalid callback: "+r);return i.get(r)}return i}},fm=sm("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),lm=sm("application/json",function(t){return JSON.parse(t.responseText)}),hm=sm("text/plain",function(t){return t.responseText}),pm=sm("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),dm=function(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=cm(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(pu(n,r=t)):r},o.row(r),i?o.get(i):o}},vm=dm("text/csv",Bd),_m=dm("text/tab-separated-values",Wd),ym=Array.prototype,gm=ym.map,mm=ym.slice,xm={name:"implicit"},bm=function(t){return function(){return t}},wm=function(t){return+t},Mm=[0,1],Tm=function(n,r,i){var o,u=n[0],a=n[n.length-1],c=e(u,a,null==r?10:r);switch(i=zv(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(u),Math.abs(a));return null!=i.precision||isNaN(o=Uv(c,s))||(i.precision=o),t.formatPrefix(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Dv(c,Math.max(Math.abs(u),Math.abs(a))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Lv(c))||(i.precision=o-2*("%"===i.type))}return t.format(i)},Nm=function(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u0?t>1?Yu(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Em:null};var Am=Em.range,Cm=1e3,zm=6e4,Pm=36e5,Rm=864e5,qm=6048e5,Lm=Yu(function(t){t.setTime(Math.floor(t/Cm)*Cm)},function(t,n){t.setTime(+t+n*Cm)},function(t,n){return(n-t)/Cm},function(t){return t.getUTCSeconds()}),Um=Lm.range,Dm=Yu(function(t){t.setTime(Math.floor(t/zm)*zm)},function(t,n){t.setTime(+t+n*zm)},function(t,n){return(n-t)/zm},function(t){return t.getMinutes()}),Om=Dm.range,Fm=Yu(function(t){var n=t.getTimezoneOffset()*zm%Pm;n<0&&(n+=Pm),t.setTime(Math.floor((+t-n)/Pm)*Pm+n)},function(t,n){t.setTime(+t+n*Pm)},function(t,n){return(n-t)/Pm},function(t){return t.getHours()}),Im=Fm.range,Ym=Yu(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*zm)/Rm},function(t){return t.getDate()-1}),Bm=Ym.range,jm=Bu(0),Hm=Bu(1),Xm=Bu(2),Vm=Bu(3),Wm=Bu(4),$m=Bu(5),Zm=Bu(6),Gm=jm.range,Jm=Hm.range,Qm=Xm.range,Km=Vm.range,tx=Wm.range,nx=$m.range,ex=Zm.range,rx=Yu(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ix=rx.range,ox=Yu(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});ox.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Yu(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var ux=ox.range,ax=Yu(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*zm)},function(t,n){return(n-t)/zm},function(t){return t.getUTCMinutes()}),cx=ax.range,sx=Yu(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+n*Pm)},function(t,n){return(n-t)/Pm},function(t){return t.getUTCHours()}),fx=sx.range,lx=Yu(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/Rm},function(t){return t.getUTCDate()-1}),hx=lx.range,px=ju(0),dx=ju(1),vx=ju(2),_x=ju(3),yx=ju(4),gx=ju(5),mx=ju(6),xx=px.range,bx=dx.range,wx=vx.range,Mx=_x.range,Tx=yx.range,Nx=gx.range,kx=mx.range,Sx=Yu(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),Ex=Sx.range,Ax=Yu(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Ax.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Yu(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Cx,zx=Ax.range,Px={"-":"",_:" ",0:"0"},Rx=/^\s*\d+/,qx=/^%/,Lx=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Ya({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Ux="%Y-%m-%dT%H:%M:%S.%LZ",Dx=Date.prototype.toISOString?Ba:t.utcFormat(Ux),Ox=+new Date("2000-01-01T00:00:00.000Z")?ja:t.utcParse(Ux),Fx=1e3,Ix=60*Fx,Yx=60*Ix,Bx=24*Yx,jx=7*Bx,Hx=30*Bx,Xx=365*Bx,Vx=function(){return Va(ox,rx,jm,Ym,Fm,Dm,Lm,Em,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])},Wx=function(){return Va(Ax,Sx,px,lx,sx,ax,Lm,Em,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])},$x=function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})},Zx=$x("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Gx=$x("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Jx=$x("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),Qx=$x("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),Kx=Uh(Xt(300,.5,0),Xt(-240,.5,1)),tb=Uh(Xt(-100,.75,.35),Xt(80,1.5,.8)),nb=Uh(Xt(260,.75,.35),Xt(80,1.5,.8)),eb=Xt(),rb=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return eb.h=360*t-100,eb.s=1.5-1.5*n,eb.l=.8-.9*n,eb+""},ib=Wa($x("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),ob=Wa($x("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),ub=Wa($x("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),ab=Wa($x("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),cb=function(t){return function(){return t}},sb=1e-12,fb=Math.PI,lb=fb/2,hb=2*fb,pb=function(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-lb,p=u.apply(this,arguments)-lb,d=Math.abs(p-h),v=p>h;if(c||(c=t=Re()),lsb)if(d>hb-sb)c.moveTo(l*Math.cos(h),l*Math.sin(h)),c.arc(0,0,l,h,p,!v),f>sb&&(c.moveTo(f*Math.cos(p),f*Math.sin(p)),c.arc(0,0,f,p,h,v));else{var _,y,g=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,N=T>sb&&(i?+i.apply(this,arguments):Math.sqrt(f*f+l*l)),k=Math.min(Math.abs(l-f)/2,+r.apply(this,arguments)),S=k,E=k;if(N>sb){var A=tc(N/f*Math.sin(T)),C=tc(N/l*Math.sin(T));(w-=2*A)>sb?(A*=v?1:-1,x+=A,b-=A):(w=0,x=b=(h+p)/2),(M-=2*C)>sb?(C*=v?1:-1,g+=C,m-=C):(M=0,g=m=(h+p)/2)}var z=l*Math.cos(g),P=l*Math.sin(g),R=f*Math.cos(b),q=f*Math.sin(b);if(k>sb){var L=l*Math.cos(m),U=l*Math.sin(m),D=f*Math.cos(x),O=f*Math.sin(x);if(dsb?nc(z,P,D,O,L,U,R,q):[R,q],I=z-F[0],Y=P-F[1],B=L-F[0],j=U-F[1],H=1/Math.sin(Math.acos((I*B+Y*j)/(Math.sqrt(I*I+Y*Y)*Math.sqrt(B*B+j*j)))/2),X=Math.sqrt(F[0]*F[0]+F[1]*F[1]);S=Math.min(k,(f-X)/(H-1)),E=Math.min(k,(l-X)/(H+1))}}M>sb?E>sb?(_=ec(D,O,z,P,l,E,v),y=ec(L,U,R,q,l,E,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),Esb&&w>sb?S>sb?(_=ec(R,q,L,U,f,-S,v),y=ec(z,P,D,O,f,-S,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),S=f;--l)s.point(_[l],y[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),y[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):y[n]))}if(p)return s=null,p+""||null}function n(){return vb().defined(u).curve(c).context(a)}var e=ic,r=null,i=cb(0),o=oc,u=cb(!0),a=null,c=db,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:cb(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:cb(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:cb(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:cb(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:cb(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:cb(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:cb(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t},yb=function(t,n){return nt?1:n>=t?0:NaN},gb=function(t){return t},mb=function(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),y=Math.min(hb,Math.max(-hb,o.apply(this,arguments)-_)),g=Math.min(Math.abs(y)/h,u.apply(this,arguments)),m=g*(y<0?-1:1);for(a=0;a0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(y-h*m)/p:0;a0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:g};return v}var n=gb,e=yb,r=null,i=cb(0),o=cb(hb),u=cb(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:cb(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:cb(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:cb(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:cb(+n),t):u},t},xb=ac(db);uc.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var bb=function(){return cc(vb().curve(xb))},wb=function(){var t=_b().curve(xb),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return cc(e())},delete t.lineX0,t.lineEndAngle=function(){return cc(r())},delete t.lineX1,t.lineInnerRadius=function(){return cc(i())},delete t.lineY0,t.lineOuterRadius=function(){return cc(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(ac(t)):n()._curve},t},Mb={draw:function(t,n){var e=Math.sqrt(n/fb);t.moveTo(e,0),t.arc(0,0,e,0,hb)}},Tb={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Nb=Math.sqrt(1/3),kb=2*Nb,Sb={draw:function(t,n){var e=Math.sqrt(n/kb),r=e*Nb;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Eb=.8908130915292852,Ab=Math.sin(fb/10)/Math.sin(7*fb/10),Cb=Math.sin(hb/10)*Ab,zb=-Math.cos(hb/10)*Ab,Pb={draw:function(t,n){var e=Math.sqrt(n*Eb),r=Cb*e,i=zb*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=hb*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},Rb={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},qb=Math.sqrt(3),Lb={draw:function(t,n){var e=-Math.sqrt(n/(3*qb));t.moveTo(0,2*e),t.lineTo(-qb*e,-e),t.lineTo(qb*e,-e),t.closePath()}},Ub=-.5,Db=Math.sqrt(3)/2,Ob=1/Math.sqrt(12),Fb=3*(Ob/2+1),Ib={draw:function(t,n){var e=Math.sqrt(n/Fb),r=e/2,i=e*Ob,o=r,u=e*Ob+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Ub*r-Db*i,Db*r+Ub*i),t.lineTo(Ub*o-Db*u,Db*o+Ub*u),t.lineTo(Ub*a-Db*c,Db*a+Ub*c),t.lineTo(Ub*r+Db*i,Ub*i-Db*r),t.lineTo(Ub*o+Db*u,Ub*u-Db*o),t.lineTo(Ub*a+Db*c,Ub*c-Db*a),t.closePath()}},Yb=[Mb,Tb,Sb,Rb,Pb,Lb,Ib],Bb=function(){function t(){var t;if(r||(r=t=Re()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=cb(Mb),e=cb(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:cb(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:cb(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},jb=function(){};fc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:sc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:sc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Hb=function(t){return new fc(t)};lc.prototype={areaStart:jb,areaEnd:jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:sc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Xb=function(t){return new lc(t)};hc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:sc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Vb=function(t){return new hc(t)};pc.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Wb=function t(n){function e(t){return 1===n?new fc(t):new pc(t,n)}return e.beta=function(n){return t(+n)},e}(.85);vc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:dc(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:dc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var $b=function t(n){function e(t){return new vc(t,n)}return e.tension=function(n){return t(+n)},e}(0);_c.prototype={areaStart:jb,areaEnd:jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:dc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Zb=function t(n){function e(t){return new _c(t,n)}return e.tension=function(n){return t(+n)},e}(0);yc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:dc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Gb=function t(n){function e(t){return new yc(t,n)}return e.tension=function(n){return t(+n)},e}(0);mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:gc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Jb=function t(n){function e(t){return n?new mc(t,n):new vc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);xc.prototype={areaStart:jb,areaEnd:jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:gc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Qb=function t(n){function e(t){return n?new xc(t,n):new _c(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);bc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0; +},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:gc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Kb=function t(n){function e(t){return n?new bc(t,n):new yc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);wc.prototype={areaStart:jb,areaEnd:jb,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};var tw=function(t){return new wc(t)};Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:kc(this,this._t0,Nc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,kc(this,Nc(this,e=Tc(this,t,n)),e);break;default:kc(this,this._t0,e=Tc(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Ec.prototype=Object.create(Sc.prototype)).point=function(t,n){Sc.prototype.point.call(this,n,t)},Ac.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Pc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=Rc(t),i=Rc(n),o=0,u=1;u=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var ew=function(t){return new qc(t,.5)},rw=Array.prototype.slice,iw=function(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i=0;)e[n]=n;return e},uw=function(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o0){for(var e,r,i,o=0,u=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u=a)return null;var c=t-i.site[0],s=n-i.site[1],f=c*c+s*s;do i=o.cells[r=u],u=null,i.halfedges.forEach(function(e){var r=o.edges[e],a=r.left;if(a!==i.site&&a||(a=r.right)){var c=t-a[0],s=n-a[1],l=c*c+s*s;le?(e+r)/2:Math.min(0,e)||Math.max(0,r),o>i?(i+o)/2:Math.min(0,i)||Math.max(0,o))}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=m.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=E(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new gs(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=A.length;r0?pl(this).transition().duration(k).call(u,f,a):pl(this).call(n.transform,f)}}function h(){if(g.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,c=u.length;for(xs(),e=0;e Date: Fri, 24 Mar 2017 15:15:01 +0300 Subject: [PATCH 02/37] Move css styles to public/sass/components/_panel_heatmap.scss --- .../panel/heatmap/css/heatmap.dark.css | 70 ------------------- .../panel/heatmap/css/heatmap.light.css | 58 --------------- .../plugins/panel/heatmap/heatmap_tooltip.ts | 4 +- public/app/plugins/panel/heatmap/module.ts | 5 -- public/sass/_grafana.scss | 1 + public/sass/components/_panel_heatmap.scss | 37 ++++++++++ 6 files changed, 40 insertions(+), 135 deletions(-) delete mode 100644 public/app/plugins/panel/heatmap/css/heatmap.dark.css delete mode 100644 public/app/plugins/panel/heatmap/css/heatmap.light.css create mode 100644 public/sass/components/_panel_heatmap.scss diff --git a/public/app/plugins/panel/heatmap/css/heatmap.dark.css b/public/app/plugins/panel/heatmap/css/heatmap.dark.css deleted file mode 100644 index 1a2ba8b7eb0..00000000000 --- a/public/app/plugins/panel/heatmap/css/heatmap.dark.css +++ /dev/null @@ -1,70 +0,0 @@ -.axis text { -} - -.axis { - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: smaller; - fill: #D8D9DA; -} - -.axis path, -.axis line { - fill: none; - stroke: #7B7B7B; - /*shape-rendering: crispEdges;*/ -} - -.axis .domain { - /*opacity: 0;*/ -} - -.tick line { - opacity: 0.4; - stroke: #7B7B7B; -} - -.tick text { - fill: #D8D9DA; -} - -.heatmap-panel { - cursor: crosshair; - padding: 0; -} - -div.heatmap-tooltip { - position: absolute; - text-align: left; - min-width: 160px; - padding: 12px; - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 13px; - background: #141414; - border: 0px; - border-radius: 8px; - pointer-events: none; -} - -.card-highlighted:hover { - stroke: #D8D9DA; -} - -rect.heatmap-card { - pointer-events: all; -} - -.heatmap-histogram rect { - fill: #828282; -} - -.heatmap-crosshair line { - stroke: #9a1010; - stroke-width: 1; -} - -.heatmap-selection { - stroke-width: 1; - opacity: 0.3; - fill: #828282; - stroke: #D8D9DA; -} diff --git a/public/app/plugins/panel/heatmap/css/heatmap.light.css b/public/app/plugins/panel/heatmap/css/heatmap.light.css deleted file mode 100644 index bf9504fc411..00000000000 --- a/public/app/plugins/panel/heatmap/css/heatmap.light.css +++ /dev/null @@ -1,58 +0,0 @@ -.axis { - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: smaller; - fill: #555555; -} - -.axis path, -.axis line { - fill: none; - stroke: #D8D9DA; - /*shape-rendering: crispEdges;*/ -} - -.tick line { - opacity: 0.4; - stroke: #D8D9DA; -} - -.tick text { - fill: #555555; -} - -.heatmap-panel { - cursor: crosshair; -} - -div.heatmap-tooltip { - position: absolute; - text-align: left; - min-width: 160px; - padding: 12px; - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 13px; - background: #ECECEC; - border: 0px; - border-radius: 8px; - pointer-events: none; -} - -.card-highlighted:hover { - stroke: #D8D9DA; -} - -.heatmap-histogram rect { - fill: #555555; -} - -.heatmap-crosshair line { - stroke: #a25959; - stroke-width: 1; -} - -.heatmap-selection { - stroke-width: 1; - opacity: 0.3; - fill: #555555; - stroke: #000; -} diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 62d03a503c3..54cede5c4ab 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -52,7 +52,7 @@ export class HeatmapTooltip { add() { this.tooltip = d3.select("body") .append("div") - .attr("class", "heatmap-tooltip"); + .attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip"); } destroy() { @@ -82,7 +82,7 @@ export class HeatmapTooltip { let decimals = this.panel.tooltipDecimals || 5; let valueFormatter = this.valueFormatter(decimals); - let tooltipHtml = `
${time}
+ let tooltipHtml = `
${time}
`; if (yData) { diff --git a/public/app/plugins/panel/heatmap/module.ts b/public/app/plugins/panel/heatmap/module.ts index 2715b95f916..3144e335a39 100644 --- a/public/app/plugins/panel/heatmap/module.ts +++ b/public/app/plugins/panel/heatmap/module.ts @@ -3,11 +3,6 @@ import {loadPluginCss} from 'app/plugins/sdk'; import {HeatmapCtrl} from './heatmap_ctrl'; -// loadPluginCss({ -// dark: 'public/app/plugins/panel/heatmap/css/heatmap.dark.css', -// light: 'public/app/plugins/panel/heatmap/css/heatmap.light.css' -// }); - export { HeatmapCtrl as PanelCtrl }; diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 370a5db943a..6266bd9c98f 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -48,6 +48,7 @@ @import "components/panel_singlestat"; @import "components/panel_table"; @import "components/panel_text"; +@import "components/panel_heatmap"; @import "components/tagsinput"; @import "components/tables_lists"; @import "components/search"; diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss new file mode 100644 index 00000000000..ad27c094fea --- /dev/null +++ b/public/sass/components/_panel_heatmap.scss @@ -0,0 +1,37 @@ +.heatmap-canvas-wrapper { + position: relative; + cursor: crosshair; +} + +.heatmap-panel { + .axis .tick { + text { + fill: $text-color; + color: $text-color; + font-size: $font-size-sm; + } + + line { + opacity: 0.4; + stroke: $text-color-weak; + } + } +} + +.heatmap-tooltip { + white-space: nowrap; + font-size: $font-size-sm; + background-color: $graph-tooltip-bg; + color: $text-color; +} + +.heatmap-histogram rect { + fill: $text-color-weak; +} + +.heatmap-crosshair { + line { + stroke: darken($red,15%); + stroke-width: 1; + } +} From 00236577014c48493fd113dec51044c76ce7001c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 24 Mar 2017 15:26:27 +0300 Subject: [PATCH 03/37] Fix bucket size calculation if all values are 0; --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index c4ad9c422dc..1a41e4be8e9 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -158,7 +158,11 @@ export class HeatmapCtrl extends MetricsPanelCtrl { yBucketSize = this.panel.yAxis.splitFactor; } else { if (heatmapStats.max === heatmapStats.min) { - yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + if (heatmapStats.max) { + yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + } else { + yBucketSize = 1; + } } else { yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; } From 0a56db7745131a4992d8018e24a448fc206471c1 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Sun, 26 Mar 2017 18:22:54 +0300 Subject: [PATCH 04/37] Add unit tests for heatmap_ctrl. --- .../panel/heatmap/specs/heatmap_ctrl_specs.ts | 76 +++++++++++++++++++ public/test/test-main.js | 3 +- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts new file mode 100644 index 00000000000..1d82ba01c23 --- /dev/null +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts @@ -0,0 +1,76 @@ +/// + +import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; + +import angular from 'angular'; +import moment from 'moment'; +import {HeatmapCtrl} from '../heatmap_ctrl'; +import helpers from '../../../../../test/specs/helpers'; + +describe('HeatmapCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + beforeEach(angularMocks.module('grafana.services')); + beforeEach(angularMocks.module('grafana.controllers')); + beforeEach(angularMocks.module(function($compileProvider) { + $compileProvider.preAssignBindingsEnabled(true); + })); + + beforeEach(ctx.providePhase()); + beforeEach(ctx.createPanelController(HeatmapCtrl)); + beforeEach(() => { + ctx.ctrl.annotationsPromise = Promise.resolve({}); + ctx.ctrl.updateTimeRange(); + }); + + describe('when time series are outside range', function() { + + beforeEach(function() { + var data = [ + {target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]}, + ]; + + ctx.ctrl.range = {from: moment().valueOf(), to: moment().valueOf()}; + ctx.ctrl.onDataReceived(data); + }); + + it('should set datapointsOutside', function() { + expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range'); + }); + }); + + describe('when time series are inside range', function() { + beforeEach(function() { + var range = { + from: moment().subtract(1, 'days').valueOf(), + to: moment().valueOf() + }; + + var data = [ + {target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]]}, + ]; + + ctx.ctrl.range = range; + ctx.ctrl.onDataReceived(data); + }); + + it('should set datapointsOutside', function() { + expect(ctx.ctrl.dataWarning).to.be(null); + }); + }); + + describe('datapointsCount given 2 series', function() { + beforeEach(function() { + var data = [ + {target: 'test.cpu1', datapoints: []}, + {target: 'test.cpu2', datapoints: []}, + ]; + ctx.ctrl.onDataReceived(data); + }); + + it('should set datapointsCount warning', function() { + expect(ctx.ctrl.dataWarning.title).to.be('No data points'); + }); + }); + +}); diff --git a/public/test/test-main.js b/public/test/test-main.js index 54fbc7d2910..6fba92cc109 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -38,7 +38,8 @@ "jquery.flot.time": "vendor/flot/jquery.flot.time", "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", - "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge" + "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", + "d3": "vendor/d3/d3.js", }, packages: { From c9801595e5df3490b1a561c912ddd8e7a91de6d0 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 27 Mar 2017 19:04:29 +0300 Subject: [PATCH 05/37] heatmap: add heatmap_ctrl tests and fix errors --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 36 ++++++++++++++++++- .../plugins/panel/heatmap/heatmap_tooltip.ts | 2 ++ public/app/plugins/panel/heatmap/module.html | 5 +++ public/app/plugins/panel/heatmap/rendering.ts | 10 ++++-- public/sass/components/_panel_heatmap.scss | 4 ++- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 1a41e4be8e9..13f50eb0b5e 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -103,6 +103,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { data: any; series: any; timeSrv: any; + dataWarning: any; constructor($scope, $injector, private $rootScope, timeSrv) { super($scope, $injector); @@ -187,6 +188,29 @@ export class HeatmapCtrl extends MetricsPanelCtrl { onDataReceived(dataList) { this.series = dataList.map(this.seriesHandler.bind(this)); + + this.dataWarning = null; + const datapointsCount = _.reduce(this.series, (sum, series) => { + return sum + series.datapoints.length; + }, 0); + + if (datapointsCount === 0) { + this.dataWarning = { + title: 'No data points', + tip: 'No datapoints returned from data query' + }; + } else { + for (let series of this.series) { + if (series.isOutsideRange) { + this.dataWarning = { + title: 'Data points outside time range', + tip: 'Can be caused by timezone mismatch or missing time filter in query', + }; + break; + } + } + } + this.render(); } @@ -196,13 +220,23 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } seriesHandler(seriesData) { - var series = new TimeSeries({ + let series = new TimeSeries({ datapoints: seriesData.datapoints, alias: seriesData.target }); series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); series.minLog = getMinLog(series); + + let datapoints = seriesData.datapoints || []; + if (datapoints && datapoints.length > 0) { + let last = datapoints[datapoints.length - 1][1]; + let from = this.range.from; + if (last - from < -10000) { + series.isOutsideRange = true; + } + } + return series; } diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 54cede5c4ab..5175ceca150 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -33,6 +33,8 @@ export class HeatmapTooltip { } onMouseOver(e) { + if (!this.panel.tooltip.show || _.isEmpty(this.scope.ctrl.data.buckets)) { return; } + if (!this.tooltip) { this.add(); this.move(e); diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html index d88681a38b8..a59092b2687 100644 --- a/public/app/plugins/panel/heatmap/module.html +++ b/public/app/plugins/panel/heatmap/module.html @@ -1,5 +1,10 @@
+ +
+ {{ctrl.dataWarning.title}} +
+
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 9c5b36b8857..cbfb2a52b6b 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -761,13 +761,19 @@ export default function link(scope, elem, attrs, ctrl) { } function render() { - if (!ctrl.data || _.isEmpty(ctrl.data.buckets)) { return; } - data = ctrl.data; panel = ctrl.panel; timeRange = ctrl.range; if (setElementHeight()) { + + // Draw default axes and return if no data + if (!data || _.isEmpty(data.buckets)) { + addHeatmapCanvas(); + addAxes(); + return; + } + addHeatmap(); scope.yScale = yScale; scope.xScale = xScale; diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index ad27c094fea..cf55c91ab7c 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -1,9 +1,11 @@ .heatmap-canvas-wrapper { - position: relative; + // position: relative; cursor: crosshair; } .heatmap-panel { + position: relative; + .axis .tick { text { fill: $text-color; From ece6fd73a1f734934917dcc969edb000c04ea13c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 28 Mar 2017 17:46:10 +0300 Subject: [PATCH 06/37] heatmap: add initial rendering tests --- public/app/plugins/panel/heatmap/rendering.ts | 6 +- .../panel/heatmap/specs/renderer_specs.ts | 165 ++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/panel/heatmap/specs/renderer_specs.ts diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index cbfb2a52b6b..1d8cf1e4d9f 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -334,8 +334,10 @@ export default function link(scope, elem, attrs, ctrl) { function addHeatmapCanvas() { let heatmap_elem = $heatmap[0]; - width = heatmap_elem.clientWidth - padding.right; - height = heatmap_elem.clientHeight - padding.bottom; + + width = Math.floor($heatmap.width()) - padding.right; + height = Math.floor($heatmap.height()) - padding.bottom; + cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING; cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND; diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts new file mode 100644 index 00000000000..2e9a6a82b4f --- /dev/null +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -0,0 +1,165 @@ +/// + +import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; + +import '../module'; +import angular from 'angular'; +import $ from 'jquery'; +import _ from 'lodash'; +import helpers from 'test/specs/helpers'; +import TimeSeries from 'app/core/time_series2'; +import moment from 'moment'; +import { Emitter } from 'app/core/core'; +import rendering from '../rendering'; +import { convertToHeatMap } from '../heatmap_data_converter'; +// import d3 from 'd3'; + +describe('grafanaHeatmap', function () { + + beforeEach(angularMocks.module('grafana.core')); + + function heatmapScenario(desc, func, elementWidth = 500) { + describe(desc, function () { + var ctx: any = {}; + + ctx.setup = function (setupFunc) { + + beforeEach(angularMocks.module(function ($provide) { + $provide.value("timeSrv", new helpers.TimeSrvStub()); + })); + + beforeEach(angularMocks.inject(function ($rootScope, $compile) { + var ctrl: any = { + events: new Emitter(), + height: 200, + panel: { + heatmap: { + }, + cards: { + cardPadding: null, + cardRound: null + }, + color: { + mode: 'color', + cardColor: '#b4ff00', + colorScale: 'linear', + exponent: 0.5, + colorScheme: 'interpolateSpectral', + fillBackground: false + }, + xBucketSize: null, + xBucketNumber: null, + yBucketSize: null, + yBucketNumber: null, + xAxis: { + show: true + }, + yAxis: { + show: true, + format: 'short', + decimals: null, + logBase: 1, + splitFactor: null, + min: null, + max: null, + removeZeroValues: false + }, + tooltip: { + show: true, + seriesStat: false, + showHistogram: false + }, + highlightCards: true + }, + renderingCompleted: sinon.spy(), + hiddenSeries: {}, + dashboard: { + getTimezone: sinon.stub().returns('browser') + }, + range: { + from: moment(1422774000000), + to: moment(1422774100000), + }, + }; + + var scope = $rootScope.$new(); + scope.ctrl = ctrl; + + let series = []; + series.push(new TimeSeries({ + datapoints: [[1422774000000, 1], [1422774060000, 2]], + alias: 'series1' + })); + series.push(new TimeSeries({ + datapoints: [[1422774000000, 2], [1422774060000, 3]], + alias: 'series2' + })); + + setupFunc(ctrl, ctx.data); + + let xBucketSize = ctrl.panel.xBucketSize; + let yBucketSize = ctrl.panel.yBucketSize; + let logBase = ctrl.panel.yAxis.logBase; + let bucketsData = convertToHeatMap(series, yBucketSize, xBucketSize, logBase); + // console.log("bucketsData", bucketsData); + + ctx.data = { + buckets: bucketsData, + heatmapStats: { + min: 1, + max: 3, + minLog: 1 + }, + xBucketSize: xBucketSize, + yBucketSize: yBucketSize + }; + + let elemHtml = ` +
+
+
+
+
`; + + var element = angular.element(elemHtml); + $compile(element)(scope); + scope.$digest(); + + ctrl.data = ctx.data; + ctx.element = element; + let render = rendering(scope, $(element), [], ctrl); + ctrl.events.emit('render'); + })); + }; + + func(ctx); + }); + } + + heatmapScenario('default options', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.logBase = 1; + ctrl.panel.xBucketSize = 1000; + ctrl.panel.yBucketSize = 1; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['1', '2', '3']); + }); + + it('should draw correct X axis', function () { + var xTicks = getTicks(ctx.element, ".axis-x"); + expect(xTicks).to.eql(['10:00:00', '10:00:15', '10:00:30', '10:00:45', '10:01:00', '10:01:15', '10:01:30']); + }); + }); + +}); + + +function getTicks(element, axisSelector) { + return element.find(axisSelector).find("text") + .map(function () { + return this.textContent; + }).get(); +} From ac347fa06a3472158357bde38e04d7ba867743d2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 28 Mar 2017 18:09:15 +0300 Subject: [PATCH 07/37] heatmap: fix error while loading panel in editor mode --- public/app/plugins/panel/heatmap/rendering.ts | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 1d8cf1e4d9f..afc92375018 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -769,26 +769,30 @@ export default function link(scope, elem, attrs, ctrl) { if (setElementHeight()) { - // Draw default axes and return if no data - if (!data || _.isEmpty(data.buckets)) { - addHeatmapCanvas(); - addAxes(); + if (data) { + // Draw default axes and return if no data + if (_.isEmpty(data.buckets)) { + addHeatmapCanvas(); + addAxes(); + return; + } + + addHeatmap(); + scope.yScale = yScale; + scope.xScale = xScale; + scope.yAxisWidth = yAxisWidth; + scope.xAxisHeight = xAxisHeight; + scope.chartHeight = chartHeight; + scope.chartWidth = chartWidth; + scope.chartTop = chartTop; + + // Register selection listeners + $heatmap.on("mousedown", onMouseDown); + $heatmap.on("mousemove", onMouseMove); + $heatmap.on("mouseleave", onMouseLeave); + } else { return; } - - addHeatmap(); - scope.yScale = yScale; - scope.xScale = xScale; - scope.yAxisWidth = yAxisWidth; - scope.xAxisHeight = xAxisHeight; - scope.chartHeight = chartHeight; - scope.chartWidth = chartWidth; - scope.chartTop = chartTop; - - // Register selection listeners - $heatmap.on("mousedown", onMouseDown); - $heatmap.on("mousemove", onMouseMove); - $heatmap.on("mouseleave", onMouseLeave); } // Draw only if color editor is opened From 72e313a3ddcb1d8d124c5b3f01ccac1cad2978d9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Mar 2017 11:29:58 +0300 Subject: [PATCH 08/37] heatmap: fix type error on buckets object --- public/app/plugins/panel/heatmap/heatmap_data_converter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 38d1818a24e..caebcad94e6 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -304,7 +304,7 @@ function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) { * @return {Object} Merged buckets. */ function mergeBuckets(seriesBuckets) { - let mergedBuckets = {}; + let mergedBuckets: any = {}; _.forEach(seriesBuckets, (seriesBucket, index) => { if (index === 0) { mergedBuckets = seriesBucket; From 7770c38e300b38516d08cdca9be434926fca668c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Mar 2017 11:30:30 +0300 Subject: [PATCH 09/37] heatmap: test for Y axis rendering --- .../panel/heatmap/specs/renderer_specs.ts | 128 +++++++++++++++--- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 2e9a6a82b4f..d94f3128339 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -47,9 +47,9 @@ describe('grafanaHeatmap', function () { colorScheme: 'interpolateSpectral', fillBackground: false }, - xBucketSize: null, + xBucketSize: 1000, xBucketNumber: null, - yBucketSize: null, + yBucketSize: 1, yBucketNumber: null, xAxis: { show: true @@ -85,35 +85,35 @@ describe('grafanaHeatmap', function () { var scope = $rootScope.$new(); scope.ctrl = ctrl; - let series = []; - series.push(new TimeSeries({ - datapoints: [[1422774000000, 1], [1422774060000, 2]], + ctx.series = []; + ctx.series.push(new TimeSeries({ + datapoints: [[1, 1422774000000], [2, 1422774060000]], alias: 'series1' })); - series.push(new TimeSeries({ - datapoints: [[1422774000000, 2], [1422774060000, 3]], + ctx.series.push(new TimeSeries({ + datapoints: [[2, 1422774000000], [3, 1422774060000]], alias: 'series2' })); - setupFunc(ctrl, ctx.data); - - let xBucketSize = ctrl.panel.xBucketSize; - let yBucketSize = ctrl.panel.yBucketSize; - let logBase = ctrl.panel.yAxis.logBase; - let bucketsData = convertToHeatMap(series, yBucketSize, xBucketSize, logBase); - // console.log("bucketsData", bucketsData); - ctx.data = { - buckets: bucketsData, heatmapStats: { min: 1, max: 3, minLog: 1 }, - xBucketSize: xBucketSize, - yBucketSize: yBucketSize + xBucketSize: ctrl.panel.xBucketSize, + yBucketSize: ctrl.panel.yBucketSize }; + setupFunc(ctrl, ctx); + + let logBase = ctrl.panel.yAxis.logBase; + let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase); + ctx.data.buckets = bucketsData; + + // console.log("bucketsData", bucketsData); + // console.log("series", ctrl.panel.yAxis.logBase, ctx.series.length); + let elemHtml = `
@@ -138,9 +138,7 @@ describe('grafanaHeatmap', function () { heatmapScenario('default options', function (ctx) { ctx.setup(function (ctrl) { - ctrl.panel.logBase = 1; - ctrl.panel.xBucketSize = 1000; - ctrl.panel.yBucketSize = 1; + ctrl.panel.yAxis.logBase = 1; }); it('should draw correct Y axis', function () { @@ -154,6 +152,94 @@ describe('grafanaHeatmap', function () { }); }); + heatmapScenario('when logBase is 2', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.yAxis.logBase = 2; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['1', '2', '4']); + }); + }); + + heatmapScenario('when logBase is 10', function (ctx) { + ctx.setup(function (ctrl, ctx) { + ctrl.panel.yAxis.logBase = 10; + + ctx.series.push(new TimeSeries({ + datapoints: [[10, 1422774000000], [20, 1422774060000]], + alias: 'series3' + })); + ctx.data.heatmapStats.max = 20; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['1', '10', '100']); + }); + }); + + heatmapScenario('when logBase is 32', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.yAxis.logBase = 32; + + ctx.series.push(new TimeSeries({ + datapoints: [[10, 1422774000000], [100, 1422774060000]], + alias: 'series3' + })); + ctx.data.heatmapStats.max = 100; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['1', '32', '1 K']); + }); + }); + + heatmapScenario('when logBase is 1024', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.yAxis.logBase = 1024; + + ctx.series.push(new TimeSeries({ + datapoints: [[2000, 1422774000000], [300000, 1422774060000]], + alias: 'series3' + })); + ctx.data.heatmapStats.max = 300000; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['1', '1 K', '1 Mil']); + }); + }); + + heatmapScenario('when Y axis format set to "none"', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.yAxis.logBase = 1; + ctrl.panel.yAxis.format = "none"; + ctx.data.heatmapStats.max = 10000; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['0', '2000', '4000', '6000', '8000', '10000', '12000']); + }); + }); + + heatmapScenario('when Y axis format set to "second"', function (ctx) { + ctx.setup(function (ctrl) { + ctrl.panel.yAxis.logBase = 1; + ctrl.panel.yAxis.format = "s"; + ctx.data.heatmapStats.max = 3600; + }); + + it('should draw correct Y axis', function () { + var yTicks = getTicks(ctx.element, ".axis-y"); + expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1 hour']); + }); + }); + }); From 7c5b9f8c5b6a5cdf0f44a1c21eb6c7578a1d3b44 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Mar 2017 12:41:34 +0300 Subject: [PATCH 10/37] heatmap tooltip: change "values" label to "count" --- public/app/plugins/panel/heatmap/heatmap_tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 5175ceca150..c78fb35cfba 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -93,7 +93,7 @@ export class HeatmapTooltip { valuesNumber = yData.values.length; tooltipHtml += `
bucket: ${boundBottom} - ${boundTop}
- values: ${valuesNumber}
+ count: ${valuesNumber}
`; if (this.panel.tooltip.seriesStat && yData.seriesStat) { From 65a829b64d70380cf774c60d9848ed3d776cb45f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Mar 2017 13:41:19 +0300 Subject: [PATCH 11/37] heatmap tests: fix timezone-dependent test error --- .../panel/heatmap/specs/renderer_specs.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index d94f3128339..1ba1bec9b83 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -74,11 +74,11 @@ describe('grafanaHeatmap', function () { renderingCompleted: sinon.spy(), hiddenSeries: {}, dashboard: { - getTimezone: sinon.stub().returns('browser') + getTimezone: sinon.stub().returns('utc') }, range: { - from: moment(1422774000000), - to: moment(1422774100000), + from: moment.utc("01 Mar 2017 10:00:00"), + to: moment.utc("01 Mar 2017 11:00:00"), }, }; @@ -148,7 +148,14 @@ describe('grafanaHeatmap', function () { it('should draw correct X axis', function () { var xTicks = getTicks(ctx.element, ".axis-x"); - expect(xTicks).to.eql(['10:00:00', '10:00:15', '10:00:30', '10:00:45', '10:01:00', '10:01:15', '10:01:30']); + let expectedTicks = [ + formatLocalTime("01 Mar 2017 10:00:00"), + formatLocalTime("01 Mar 2017 10:15:00"), + formatLocalTime("01 Mar 2017 10:30:00"), + formatLocalTime("01 Mar 2017 10:45:00"), + formatLocalTime("01 Mar 2017 11:00:00") + ]; + expect(xTicks).to.eql(expectedTicks); }); }); @@ -249,3 +256,8 @@ function getTicks(element, axisSelector) { return this.textContent; }).get(); } + +function formatLocalTime(timeStr) { + let format = "HH:mm"; + return moment.utc(timeStr).local().format(format); +} From c33fa27ed8848e93ea45436c8c5843917d1e21ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 29 Mar 2017 17:28:52 +0200 Subject: [PATCH 12/37] Update latest.json --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 75f20d75a69..23ba2ba5b75 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "4.1.1", - "testing": "4.1.1" + "stable": "4.2.0", + "testing": "4.2.0" } From aada5181fa4451c797b450fb850259564961cc10 Mon Sep 17 00:00:00 2001 From: Kazumasa Kohtaka Date: Thu, 30 Mar 2017 15:54:32 +0900 Subject: [PATCH 13/37] docs: fix typo in a section describing search API (#7984) --- docs/sources/http_api/dashboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 5fed69b4a93..12d46dc6aa4 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -232,7 +232,7 @@ Get all tags of dashboards Status Codes: - **query** – Search Query -- **tags** – Tags to use +- **tag** – Tag to use - **starred** – Flag indicating if only starred Dashboards should be returned - **tagcloud** - Flag indicating if a tagcloud should be returned From 6e304e6e4256fde8d81b9ba6d75777562f44c1ab Mon Sep 17 00:00:00 2001 From: timolehto Date: Thu, 30 Mar 2017 18:12:00 +0300 Subject: [PATCH 14/37] comments: Typos in defaults.ini (#7988) --- conf/defaults.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 5c12937f4f8..9955c22ca17 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -60,14 +60,14 @@ cert_key = #################################### Database ############################ [database] # You can configure the database connection by specifying type, host, name, user and password -# as seperate properties or as on string using the url propertie. +# as separate properties or as on string using the url property. # Either "mysql", "postgres" or "sqlite3", it's your choice type = sqlite3 host = 127.0.0.1:3306 name = grafana user = root -# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" password = # Use either URL or the previous fields to configure the database # Example: mysql://user:secret@host:port/database @@ -132,7 +132,7 @@ logging = false reporting_enabled = true # Set to false to disable all checks to https://grafana.com -# for new vesions (grafana itself and plugins), check is used +# for new versions (grafana itself and plugins), check is used # in some UI views to notify that grafana or plugin update exists # This option does not cause any auto updates, nor send any information # only a GET request to https://grafana.com to get latest versions From 114431beed6ce37c1c3244beace2627019b17fcc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Mar 2017 18:21:22 +0300 Subject: [PATCH 15/37] heatmap: add tests for heatmap_data_converter --- .../panel/heatmap/heatmap_data_converter.ts | 52 +++++++- .../specs/heatmap_data_converter_specs.ts | 125 ++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index caebcad94e6..9c4805bdf9d 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -5,6 +5,16 @@ import _ from 'lodash'; let VALUE_INDEX = 0; let TIME_INDEX = 1; +interface XBucket { + x: number; + buckets: any; +} + +interface YBucket { + y: number; + values: number[]; +} + /** * Convert set of time series into heatmap buckets * @return {Object} Heatmap object: @@ -347,11 +357,51 @@ function logp(value, base) { return Math.log(value) / Math.log(base); } +/** + * Compare two heatmap data objects + * @param objA + * @param objB + */ +function isHeatmapDataEqual(objA: any, objB: any): boolean { + let is_eql = true; + + _.forEach(objA, (xBucket: XBucket, x) => { + if (objB[x]) { + _.forEach(xBucket.buckets, (yBucket: YBucket, y) => { + if (objB[x].buckets && objB[x].buckets[y]) { + if (objB[x].buckets[y].values) { + is_eql = _.isEqual(yBucket.values.sort(), objB[x].buckets[y].values.sort()); + if (!is_eql) { + return false; + } + } else { + is_eql = false; + return false; + } + } else { + is_eql = false; + return false; + } + }); + + if (!is_eql) { + return false; + } + } else { + is_eql = false; + return false; + } + }); + + return is_eql; +} + export { convertToHeatMap, convertToCards, removeZeroBuckets, mergeZeroBuckets, getMinLog, - getValueBucketBound + getValueBucketBound, + isHeatmapDataEqual }; diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts new file mode 100644 index 00000000000..389b40b38bb --- /dev/null +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts @@ -0,0 +1,125 @@ +/// + +import _ from 'lodash'; +import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; +import TimeSeries from 'app/core/time_series2'; +import { convertToHeatMap, isHeatmapDataEqual } from '../heatmap_data_converter'; + +describe('isHeatmapDataEqual', () => { + let ctx: any = {}; + + beforeEach(() => { + ctx.heatmapA = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1, 1.5] }, + '2': { y: 2, values: [1] } + } + } + }; + + ctx.heatmapB = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1.5, 1] }, + '2': { y: 2, values: [1] } + } + } + }; + }); + + it('should proper compare objects', () => { + let heatmapC = _.cloneDeep(ctx.heatmapA); + heatmapC['1422774000000'].buckets['1'].values = [1, 1.5]; + + let heatmapD = _.cloneDeep(ctx.heatmapA); + heatmapD['1422774000000'].buckets['1'].values = [1.5, 1, 1.6]; + + let heatmapE = _.cloneDeep(ctx.heatmapA); + heatmapE['1422774000000'].buckets['1'].values = [1, 1.6]; + + expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).to.be(true); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapC)).to.be(true); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapD)).to.be(false); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapE)).to.be(false); + }); +}); + +describe('HeatmapDataConverter', () => { + let ctx: any = {}; + + beforeEach(() => { + ctx.series = []; + ctx.series.push(new TimeSeries({ + datapoints: [[1, 1422774000000], [2, 1422774060000]], + alias: 'series1' + })); + ctx.series.push(new TimeSeries({ + datapoints: [[2, 1422774000000], [3, 1422774060000]], + alias: 'series2' + })); + + ctx.xBucketSize = 60000; // 60s + ctx.yBucketSize = 1; + ctx.logBase = 1; + }); + + describe('when logBase is 1 (linear scale)', () => { + + beforeEach(() => { + ctx.logBase = 1; + }); + + it('should build proper heatmap data', () => { + let expectedHeatmap = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1] }, + '2': { y: 2, values: [2] } + } + }, + '1422774060000': { + x: 1422774060000, + buckets: { + '2': { y: 2, values: [2] }, + '3': { y: 3, values: [3] } + } + }, + }; + + let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true); + }); + }); + + describe('when logBase is 2', () => { + + beforeEach(() => { + ctx.logBase = 2; + }); + + it('should build proper heatmap data', () => { + let expectedHeatmap = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1] }, + '2': { y: 2, values: [2] } + } + }, + '1422774060000': { + x: 1422774060000, + buckets: { + '2': { y: 2, values: [2, 3] } + } + }, + }; + + let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true); + }); + }); +}); From 3170f0d84ac3615e9d5ec7a98eb82227424817cc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Mar 2017 20:55:10 +0300 Subject: [PATCH 16/37] heatmap: add ES histogram converter --- .../panel/heatmap/heatmap_data_converter.ts | 72 +++++++++++++++++-- .../specs/heatmap_data_converter_specs.ts | 69 +++++++++++++++++- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 9c4805bdf9d..add09f6b3ba 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -1,6 +1,7 @@ /// import _ from 'lodash'; +import TimeSeries from 'app/core/time_series2'; let VALUE_INDEX = 0; let TIME_INDEX = 1; @@ -15,6 +16,48 @@ interface YBucket { values: number[]; } +function elasticHistogramToHeatmap(series) { + let seriesBuckets = _.map(series, (s: TimeSeries) => { + return convertEsSeriesToHeatmap(s); + }); + let buckets = mergeBuckets(seriesBuckets); + return buckets; +} + +function convertEsSeriesToHeatmap(series: TimeSeries, saveZeroCounts = false) { + let xBuckets: XBucket[] = []; + + _.forEach(series.datapoints, point => { + let bound = series.alias; + let count = point[VALUE_INDEX]; + let values = new Array(count); + values.fill(Number(bound)); + + let valueBuckets = {}; + valueBuckets[bound] = { + y: Number(bound), + values: values + }; + + let xBucket: XBucket = { + x: point[TIME_INDEX], + buckets: valueBuckets + }; + + // Don't push buckets vith 0 count until saveZeroCounts flag is set + if (count !== 0 || (count === 0 && saveZeroCounts)) { + xBuckets.push(xBucket); + } + }); + + let heatmap: any = {}; + _.forEach(xBuckets, (bucket: XBucket) => { + heatmap[bucket.x] = bucket; + }); + + return heatmap; +} + /** * Convert set of time series into heatmap buckets * @return {Object} Heatmap object: @@ -321,20 +364,29 @@ function mergeBuckets(seriesBuckets) { } else { _.forEach(seriesBucket, (xBucket, xBound) => { if (mergedBuckets[xBound]) { - mergedBuckets[xBound].points = xBucket.points.concat(mergedBuckets[xBound].points); - mergedBuckets[xBound].values = xBucket.values.concat(mergedBuckets[xBound].values); + if (xBucket.points) { + mergedBuckets[xBound].points = xBucket.points.concat(mergedBuckets[xBound].points); + } + if (xBucket.values) { + mergedBuckets[xBound].values = xBucket.values.concat(mergedBuckets[xBound].values); + } _.forEach(xBucket.buckets, (yBucket, yBound) => { let bucket = mergedBuckets[xBound].buckets[yBound]; if (bucket && bucket.values) { mergedBuckets[xBound].buckets[yBound].values = bucket.values.concat(yBucket.values); - mergedBuckets[xBound].buckets[yBound].points = bucket.points.concat(yBucket.points); + + if (bucket.points) { + mergedBuckets[xBound].buckets[yBound].points = bucket.points.concat(yBucket.points); + } } else { mergedBuckets[xBound].buckets[yBound] = yBucket; } let points = mergedBuckets[xBound].buckets[yBound].points; - mergedBuckets[xBound].buckets[yBound].seriesStat = getSeriesStat(points); + if (points) { + mergedBuckets[xBound].buckets[yBound].seriesStat = getSeriesStat(points); + } }); } else { mergedBuckets[xBound] = xBucket; @@ -363,10 +415,15 @@ function logp(value, base) { * @param objB */ function isHeatmapDataEqual(objA: any, objB: any): boolean { - let is_eql = true; + let is_eql = !emptyXOR(objA, objB); _.forEach(objA, (xBucket: XBucket, x) => { if (objB[x]) { + if (emptyXOR(xBucket.buckets, objB[x].buckets)) { + is_eql = false; + return false; + } + _.forEach(xBucket.buckets, (yBucket: YBucket, y) => { if (objB[x].buckets && objB[x].buckets[y]) { if (objB[x].buckets[y].values) { @@ -396,8 +453,13 @@ function isHeatmapDataEqual(objA: any, objB: any): boolean { return is_eql; } +function emptyXOR(foo: any, bar: any): boolean { + return (_.isEmpty(foo) || _.isEmpty(bar)) && !(_.isEmpty(foo) && _.isEmpty(bar)); +} + export { convertToHeatMap, + elasticHistogramToHeatmap, convertToCards, removeZeroBuckets, mergeZeroBuckets, diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts index 389b40b38bb..2f37fd04dd2 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; import TimeSeries from 'app/core/time_series2'; -import { convertToHeatMap, isHeatmapDataEqual } from '../heatmap_data_converter'; +import { convertToHeatMap, elasticHistogramToHeatmap, isHeatmapDataEqual } from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { let ctx: any = {}; @@ -40,10 +40,27 @@ describe('isHeatmapDataEqual', () => { let heatmapE = _.cloneDeep(ctx.heatmapA); heatmapE['1422774000000'].buckets['1'].values = [1, 1.6]; + let empty = {}; + let emptyValues = _.cloneDeep(ctx.heatmapA); + emptyValues['1422774000000'].buckets['1'].values = []; + expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).to.be(true); + expect(isHeatmapDataEqual(ctx.heatmapB, ctx.heatmapA)).to.be(true); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapC)).to.be(true); + expect(isHeatmapDataEqual(heatmapC, ctx.heatmapA)).to.be(true); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapD)).to.be(false); + expect(isHeatmapDataEqual(heatmapD, ctx.heatmapA)).to.be(false); + expect(isHeatmapDataEqual(ctx.heatmapA, heatmapE)).to.be(false); + expect(isHeatmapDataEqual(heatmapE, ctx.heatmapA)).to.be(false); + + expect(isHeatmapDataEqual(empty, ctx.heatmapA)).to.be(false); + expect(isHeatmapDataEqual(ctx.heatmapA, empty)).to.be(false); + + expect(isHeatmapDataEqual(emptyValues, ctx.heatmapA)).to.be(false); + expect(isHeatmapDataEqual(ctx.heatmapA, emptyValues)).to.be(false); }); }); @@ -123,3 +140,53 @@ describe('HeatmapDataConverter', () => { }); }); }); + +describe('ES Histogram converter', () => { + let ctx: any = {}; + + beforeEach(() => { + ctx.series = []; + ctx.series.push(new TimeSeries({ + datapoints: [[1, 1422774000000], [0, 1422774060000]], + alias: '1', label: '1' + })); + ctx.series.push(new TimeSeries({ + datapoints: [[1, 1422774000000], [3, 1422774060000]], + alias: '2', label: '2' + })); + ctx.series.push(new TimeSeries({ + datapoints: [[0, 1422774000000], [1, 1422774060000]], + alias: '3', label: '3' + })); + }); + + describe('when converting ES histogram', () => { + + beforeEach(() => { + }); + + it('should build proper heatmap data', () => { + let expectedHeatmap = { + '1422774000000': { + x: 1422774000000, + buckets: { + '1': { y: 1, values: [1] }, + '2': { y: 2, values: [2] } + } + }, + '1422774060000': { + x: 1422774060000, + buckets: { + '2': { y: 2, values: [2, 2, 2] }, + '3': { y: 3, values: [3] } + } + }, + }; + + let heatmap = elasticHistogramToHeatmap(ctx.series); + console.log(heatmap); + expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true); + }); + }); +}); + From 9ddfeaa9c263feb27c9f8478319aa32b0845beb8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Mar 2017 21:36:36 +0300 Subject: [PATCH 17/37] heatmap: initial ES histogram support --- .../app/plugins/panel/heatmap/axes_editor.ts | 6 ++ .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 78 ++++++++++++++----- .../panel/heatmap/partials/axes_editor.html | 10 +++ 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/heatmap/axes_editor.ts b/public/app/plugins/panel/heatmap/axes_editor.ts index 662b1aba406..5b84f272d95 100644 --- a/public/app/plugins/panel/heatmap/axes_editor.ts +++ b/public/app/plugins/panel/heatmap/axes_editor.ts @@ -7,6 +7,7 @@ export class AxesEditorCtrl { panelCtrl: any; unitFormats: any; logScales: any; + dataFormats: any; /** @ngInject */ constructor($scope) { @@ -23,6 +24,11 @@ export class AxesEditorCtrl { 'log (base 32)': 32, 'log (base 1024)': 1024 }; + + this.dataFormats = { + 'Timeseries': 'timeseries', + 'ES histogram': 'es_histogram' + }; } setUnitFormat(subItem) { diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 13f50eb0b5e..c7572b38745 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series'; import {axesEditor} from './axes_editor'; import {heatmapDisplayEditor} from './display_editor'; import rendering from './rendering'; -import {convertToHeatMap, getMinLog} from './heatmap_data_converter'; +import { convertToHeatMap, elasticHistogramToHeatmap, getMinLog} from './heatmap_data_converter'; let X_BUCKET_NUMBER_DEFAULT = 30; let Y_BUCKET_NUMBER_DEFAULT = 10; @@ -27,6 +27,7 @@ let panelDefaults = { colorScheme: 'interpolateSpectral', fillBackground: false }, + dataFormat: 'timeseries', xBucketSize: null, xBucketNumber: null, yBucketSize: null, @@ -137,7 +138,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { onRender() { if (!this.range) { return; } - let xBucketSize, yBucketSize; + let xBucketSize, yBucketSize, heatmapStats, bucketsData; let logBase = this.panel.yAxis.logBase; let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); @@ -152,25 +153,49 @@ export class HeatmapCtrl extends MetricsPanelCtrl { xBucketSize = Number(this.panel.xBucketSize); } - // Calculate Y bucket size - let heatmapStats = this.parseSeries(this.series); - let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; - if (logBase !== 1) { - yBucketSize = this.panel.yAxis.splitFactor; - } else { - if (heatmapStats.max === heatmapStats.min) { - if (heatmapStats.max) { - yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; - } else { - yBucketSize = 1; - } - } else { - yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; - } - yBucketSize = this.panel.yBucketSize || yBucketSize; - } + if (this.panel.dataFormat === 'es_histogram') { + heatmapStats = this.parseHistogramSeries(this.series); - let bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase); + // Calculate Y bucket size + let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + if (logBase !== 1) { + yBucketSize = this.panel.yAxis.splitFactor; + } else { + if (heatmapStats.max === heatmapStats.min) { + if (heatmapStats.max) { + yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + } else { + yBucketSize = 1; + } + } else { + yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; + } + yBucketSize = this.panel.yBucketSize || yBucketSize; + } + + bucketsData = elasticHistogramToHeatmap(this.series); + } else { + + // Calculate Y bucket size + heatmapStats = this.parseSeries(this.series); + let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + if (logBase !== 1) { + yBucketSize = this.panel.yAxis.splitFactor; + } else { + if (heatmapStats.max === heatmapStats.min) { + if (heatmapStats.max) { + yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; + } else { + yBucketSize = 1; + } + } else { + yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; + } + yBucketSize = this.panel.yBucketSize || yBucketSize; + } + + bucketsData = convertToHeatMap(this.series, yBucketSize, xBucketSize, logBase); + } // Set default Y range if no data if (!heatmapStats.min && !heatmapStats.max) { @@ -252,6 +277,19 @@ export class HeatmapCtrl extends MetricsPanelCtrl { }; } + parseHistogramSeries(series) { + let bounds = _.map(series, s => Number(s.alias)); + let min = _.min(bounds); + let minLog = _.min(bounds); + let max = _.max(bounds); + + return { + max: max, + min: min, + minLog: minLog + }; + } + link(scope, elem, attrs, ctrl) { rendering(scope, elem, attrs, ctrl); } diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html index f9a0464d381..161be2dab19 100644 --- a/public/app/plugins/panel/heatmap/partials/axes_editor.html +++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html @@ -82,4 +82,14 @@ ng-model="ctrl.panel.xBucketSize" ng-change="ctrl.refresh()" ng-model-onblur>
+ +
+
Data format
+
+ +
+ +
+
+
From 554f972a25f483e22db5d74aeb3fd3a9cbe89129 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 31 Mar 2017 14:31:15 +0900 Subject: [PATCH 18/37] support panel repeat for datasource template variable (#7711) * support panel repeat for datasource template variable * support All option --- public/app/core/services/datasource_srv.js | 4 ++-- public/app/features/panel/metrics_panel_ctrl.ts | 2 +- .../features/templating/datasource_variable.ts | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/public/app/core/services/datasource_srv.js b/public/app/core/services/datasource_srv.js index e722aae355b..bb187a7d5fb 100644 --- a/public/app/core/services/datasource_srv.js +++ b/public/app/core/services/datasource_srv.js @@ -14,12 +14,12 @@ function (angular, _, coreModule, config) { this.datasources = {}; }; - this.get = function(name) { + this.get = function(name, scopedDsVars) { if (!name) { return this.get(config.defaultDatasource); } - name = templateSrv.replace(name); + name = templateSrv.replace(name, scopedDsVars || {}); if (name === 'default') { return this.get(config.defaultDatasource); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index b42d4a8b02a..6f066723741 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -92,7 +92,7 @@ class MetricsPanelCtrl extends PanelCtrl { // load datasource service this.setTimeQueryStart(); - this.datasourceSrv.get(this.panel.datasource) + this.datasourceSrv.get(this.panel.datasource, this.panel.scopedVars) .then(this.updateTimeRange.bind(this)) .then(this.issueQueries.bind(this)) .then(this.handleQueryResult.bind(this)) diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 41f2262ab4a..66b0ab2eff9 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -10,6 +10,8 @@ export class DatasourceVariable implements Variable { query: string; options: any; current: any; + multi: boolean; + includeAll: boolean; refresh: any; defaults = { @@ -21,6 +23,8 @@ export class DatasourceVariable implements Variable { regex: '', options: [], query: '', + multi: false, + includeAll: false, refresh: 1, }; @@ -71,9 +75,16 @@ export class DatasourceVariable implements Variable { } this.options = options; + if (this.includeAll) { + this.addAllOption(); + } return this.variableSrv.validateVariableSelectionState(this); } + addAllOption() { + this.options.unshift({text: 'All', value: "$__all"}); + } + dependsOn(variable) { if (this.regex) { return containsVariable(this.regex, variable.name); @@ -86,6 +97,9 @@ export class DatasourceVariable implements Variable { } getValueForUrl() { + if (this.current.text === 'All') { + return 'All'; + } return this.current.value; } } @@ -93,5 +107,6 @@ export class DatasourceVariable implements Variable { variableTypes['datasource'] = { name: 'Datasource', ctor: DatasourceVariable, + supportsMulti: true, description: 'Enabled you to dynamically switch the datasource for multiple panels', }; From af0ecd440f305a5ea99094989f7863ff47c46e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Mar 2017 07:32:44 +0200 Subject: [PATCH 19/37] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b3ba2037e..a643338979a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * **Cloudwatch**: Correctly obtain IAM roles within ECS container tasks [#7892](https://github.com/grafana/grafana/issues/7892) thx [@gomlgs](https://github.com/gomlgs) * **Units**: New number format: Scientific notation [#7781](https://github.com/grafana/grafana/issues/7781) thx [@cadnce](https://github.com/cadnce) * **Oauth**: Add common type for oauth authorization errors [#6428](https://github.com/grafana/grafana/issues/6428) thx [@amenzhinsky](https://github.com/amenzhinsky) +* **Templating**: Data source variable now supports multi value and panel repeats [#7030](https://github.com/grafana/grafana/issues/7030) thx [@mtanda](https://github.com/mtanda) # 4.2.0 (2017-03-22) ## Minor Enhancements From cb136c07b8a9333d47458a90aae18be887c1018a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Mar 2017 11:44:44 +0300 Subject: [PATCH 20/37] heatmap: add function for bucket size calculation --- .../panel/heatmap/heatmap_data_converter.ts | 53 +++++++++++++-- .../specs/heatmap_data_converter_specs.ts | 65 ++++++++++++++++++- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index add09f6b3ba..54db6be5838 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -44,7 +44,7 @@ function convertEsSeriesToHeatmap(series: TimeSeries, saveZeroCounts = false) { buckets: valueBuckets }; - // Don't push buckets vith 0 count until saveZeroCounts flag is set + // Don't push buckets with 0 count until saveZeroCounts flag is set if (count !== 0 || (count === 0 && saveZeroCounts)) { xBuckets.push(xBucket); } @@ -404,11 +404,55 @@ function getMinLog(series) { return _.min(values); } -// Logarithm for custom base +/** + * Logarithm for custom base + * @param value + * @param base logarithm base + */ function logp(value, base) { return Math.log(value) / Math.log(base); } +/** + * Calculate size of Y bucket from given buckets bounds. + * @param bounds Array of Y buckets bounds + * @param logBase Logarithm base + */ +function calculateBucketSize(bounds: number[], logBase = 1): number { + let bucketSize = Infinity; + + if (bounds.length === 0) { + return 0; + } else if (bounds.length === 1) { + return bounds[0]; + } else { + bounds = _.sortBy(bounds); + for (let i = 1; i < bounds.length; i++) { + let distance = getDistance(bounds[i], bounds[i - 1], logBase); + bucketSize = distance < bucketSize ? distance : bucketSize; + } + } + + return bucketSize; +} + +/** + * Calculate distance between two numbers in given scale (linear or logarithmic). + * @param a + * @param b + * @param logBase + */ +function getDistance(a: number, b: number, logBase = 1): number { + if (logBase === 1) { + // Linear distance + return Math.abs(b - a); + } else { + // logarithmic distance + let ratio = Math.max(a, b) / Math.min(a, b); + return logp(ratio, logBase); + } +} + /** * Compare two heatmap data objects * @param objA @@ -427,7 +471,7 @@ function isHeatmapDataEqual(objA: any, objB: any): boolean { _.forEach(xBucket.buckets, (yBucket: YBucket, y) => { if (objB[x].buckets && objB[x].buckets[y]) { if (objB[x].buckets[y].values) { - is_eql = _.isEqual(yBucket.values.sort(), objB[x].buckets[y].values.sort()); + is_eql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values)); if (!is_eql) { return false; } @@ -465,5 +509,6 @@ export { mergeZeroBuckets, getMinLog, getValueBucketBound, - isHeatmapDataEqual + isHeatmapDataEqual, + calculateBucketSize }; diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts index 2f37fd04dd2..b1019b94c56 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; import TimeSeries from 'app/core/time_series2'; -import { convertToHeatMap, elasticHistogramToHeatmap, isHeatmapDataEqual } from '../heatmap_data_converter'; +import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { let ctx: any = {}; @@ -64,6 +64,55 @@ describe('isHeatmapDataEqual', () => { }); }); +describe('calculateBucketSize', () => { + let ctx: any = {}; + + describe('when logBase is 1 (linear scale)', () => { + + beforeEach(() => { + ctx.logBase = 1; + ctx.bounds_set = [ + { bounds: [], size: 0 }, + { bounds: [0], size: 0 }, + { bounds: [4], size: 4 }, + { bounds: [0, 1, 2, 3, 4], size: 1 }, + { bounds: [0, 1, 3, 5, 7], size: 1 }, + { bounds: [0, 3, 7, 9, 15], size: 2 }, + { bounds: [0, 7, 3, 15, 9], size: 2 }, + { bounds: [0, 5, 10, 15, 50], size: 5 } + ]; + }); + + it('should properly calculate bucket size', () => { + _.each(ctx.bounds_set, (b) => { + let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + expect(bucketSize).to.be(b.size); + }); + }); + }); + + describe('when logBase is 2', () => { + + beforeEach(() => { + ctx.logBase = 2; + ctx.bounds_set = [ + { bounds: [], size: 0 }, + { bounds: [0], size: 0 }, + { bounds: [4], size: 4 }, + { bounds: [1, 2, 4, 8], size: 1 }, + { bounds: [1, Math.SQRT2, 2, 8, 16], size: 0.5 } + ]; + }); + + it('should properly calculate bucket size', () => { + _.each(ctx.bounds_set, (b) => { + let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + expect(isEqual(bucketSize, b.size)).to.be(true); + }); + }); + }); +}); + describe('HeatmapDataConverter', () => { let ctx: any = {}; @@ -184,9 +233,21 @@ describe('ES Histogram converter', () => { }; let heatmap = elasticHistogramToHeatmap(ctx.series); - console.log(heatmap); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true); }); }); }); +/** + * Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss. + * @param a + * @param b + * @param precision + */ +function isEqual(a: number, b: number, precision = 0.000001): boolean { + if (a === b) { + return true; + } else { + return Math.abs(1 - a / b) <= precision; + } +} From b18ed0b98f1918374c6e4d222cc5c4885c2ef01c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Mar 2017 12:51:43 +0300 Subject: [PATCH 21/37] heatmap: calculate bucket size automatically for ES histogram --- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 54 ++++++++----------- .../panel/heatmap/heatmap_data_converter.ts | 11 +++- public/app/plugins/panel/heatmap/rendering.ts | 2 +- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index c7572b38745..6eedbbaf9c4 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series'; import {axesEditor} from './axes_editor'; import {heatmapDisplayEditor} from './display_editor'; import rendering from './rendering'; -import { convertToHeatMap, elasticHistogramToHeatmap, getMinLog} from './heatmap_data_converter'; +import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter'; let X_BUCKET_NUMBER_DEFAULT = 30; let Y_BUCKET_NUMBER_DEFAULT = 10; @@ -140,41 +140,33 @@ export class HeatmapCtrl extends MetricsPanelCtrl { let xBucketSize, yBucketSize, heatmapStats, bucketsData; let logBase = this.panel.yAxis.logBase; - let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; - let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); - - // Parse X bucket size (number or interval) - let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); - if (isIntervalString) { - xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); - } else if (isNaN(Number(this.panel.xBucketSize)) || this.panel.xBucketSize === '' || this.panel.xBucketSize === null) { - xBucketSize = xBucketSizeByNumber; - } else { - xBucketSize = Number(this.panel.xBucketSize); - } if (this.panel.dataFormat === 'es_histogram') { heatmapStats = this.parseHistogramSeries(this.series); - - // Calculate Y bucket size - let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; - if (logBase !== 1) { - yBucketSize = this.panel.yAxis.splitFactor; - } else { - if (heatmapStats.max === heatmapStats.min) { - if (heatmapStats.max) { - yBucketSize = heatmapStats.max / Y_BUCKET_NUMBER_DEFAULT; - } else { - yBucketSize = 1; - } - } else { - yBucketSize = (heatmapStats.max - heatmapStats.min) / yBucketNumber; - } - yBucketSize = this.panel.yBucketSize || yBucketSize; - } - bucketsData = elasticHistogramToHeatmap(this.series); + + // Calculate bucket size based on ES heatmap data + let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); + let yBucketBoundSet = _.map(this.series, series => Number(series.alias)); + xBucketSize = calculateBucketSize(xBucketBoundSet); + yBucketSize = calculateBucketSize(yBucketBoundSet, logBase); + if (logBase !== 1) { + // Use yBucketSize in meaning of "Split factor" for log scales + yBucketSize = 1 / yBucketSize; + } } else { + let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; + let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); + + // Parse X bucket size (number or interval) + let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); + if (isIntervalString) { + xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); + } else if (isNaN(Number(this.panel.xBucketSize)) || this.panel.xBucketSize === '' || this.panel.xBucketSize === null) { + xBucketSize = xBucketSizeByNumber; + } else { + xBucketSize = Number(this.panel.xBucketSize); + } // Calculate Y bucket size heatmapStats = this.parseSeries(this.series); diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 54db6be5838..800eb26f322 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -155,10 +155,17 @@ function mergeZeroBuckets(buckets, minValue) { let newBucket = { y: 0, bounds: {bottom: minValue, top: minBucket.bounds.top || minValue}, - values: nullBucket.values.concat(minBucket.values), - points: nullBucket.points.concat(minBucket.points) + values: [], + points: [] }; + if (nullBucket.values) { + newBucket.values = nullBucket.values.concat(minBucket.values); + } + if (nullBucket.points) { + newBucket.points = nullBucket.points.concat(minBucket.points); + } + let newYBuckets = {}; _.forEach(yBuckets, (bucket, bound) => { bound = Number(bound); diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index afc92375018..19e6dedfdd0 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -449,7 +449,7 @@ export default function link(scope, elem, attrs, ctrl) { if (panel.yAxis.logBase !== 1) { let base = panel.yAxis.logBase; - let splitFactor = panel.yAxis.splitFactor || 1; + let splitFactor = data.yBucketSize || 1; yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor); } From 39e45004cd47b5160c28ff88d7b9add5e3d76554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Mar 2017 16:09:00 +0200 Subject: [PATCH 22/37] heatmap: inverted the color schemes --- public/app/plugins/panel/heatmap/rendering.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 19e6dedfdd0..93112dca496 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -428,7 +428,7 @@ export default function link(scope, elem, attrs, ctrl) { function setColorScale(max_value) { let colorInterpolator = d3[panel.color.colorScheme]; - colorScale = d3.scaleSequential(colorInterpolator).domain([0, max_value]); + colorScale = d3.scaleSequential(colorInterpolator).domain([max_value, 0]); } function setOpacityScale(max_value) { @@ -711,7 +711,7 @@ export default function link(scope, elem, attrs, ctrl) { let legendHeight = d3.select("#heatmap-color-legend").attr("height"); let colorInterpolator = d3[panel.color.colorScheme]; - let legendColorScale = d3.scaleSequential(colorInterpolator).domain([0, legendWidth]); + let legendColorScale = d3.scaleSequential(colorInterpolator).domain([legendWidth, 0]); let rangeStep = 2; let valuesRange = d3.range(0, legendWidth, rangeStep); From f77f8ebfc6685869f354b14109a4b558c7edc16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Mar 2017 17:12:50 +0200 Subject: [PATCH 23/37] heatmap: changes to color spectrum, inverted some on dark theme --- public/app/core/services/context_srv.ts | 1 + .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 55 ++++++++----------- .../heatmap/partials/display_editor.html | 28 ++++------ public/app/plugins/panel/heatmap/rendering.ts | 22 +++++--- .../panel/heatmap/specs/renderer_specs.ts | 8 ++- 5 files changed, 56 insertions(+), 58 deletions(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index f3d319f11c6..ddff1093720 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -11,6 +11,7 @@ export class User { orgRole: any; timezone: string; helpFlags1: number; + lightTheme: boolean; constructor() { if (config.bootData.user) { diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 6eedbbaf9c4..2777b84e2f8 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -20,11 +20,11 @@ let panelDefaults = { cardRound: null }, color: { - mode: 'color', + mode: 'spectrum', cardColor: '#b4ff00', - colorScale: 'linear', + colorScale: 'sqrt', exponent: 0.5, - colorScheme: 'interpolateSpectral', + colorScheme: 'interpolateOranges', fillBackground: false }, dataFormat: 'timeseries', @@ -53,44 +53,37 @@ let panelDefaults = { highlightCards: true }; -let colorModes = ['opacity', 'color']; +let colorModes = ['opacity', 'spectrum']; let opacityScales = ['linear', 'sqrt']; // Schemes from d3-scale-chromatic // https://github.com/d3/d3-scale-chromatic let colorSchemes = [ // Diverging - {name: 'Spectral', value: 'interpolateSpectral'}, - {name: 'BrBG', value: 'interpolateBrBG'}, - {name: 'PRGn', value: 'interpolatePRGn'}, - {name: 'PiYG', value: 'interpolatePiYG'}, - {name: 'PuOr', value: 'interpolatePuOr'}, - {name: 'RdBu', value: 'interpolateRdBu'}, - {name: 'RdGy', value: 'interpolateRdGy'}, - {name: 'RdYlBu', value: 'interpolateRdYlBu'}, - {name: 'RdYlGn', value: 'interpolateRdYlGn'}, + {name: 'Spectral', value: 'interpolateSpectral', invert: 'always'}, + {name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always'}, // Sequential (Single Hue) - {name: 'Blues', value: 'interpolateBlues'}, - {name: 'Greens', value: 'interpolateGreens'}, - {name: 'Greys', value: 'interpolateGreys'}, - {name: 'Oranges', value: 'interpolateOranges'}, - {name: 'Purples', value: 'interpolatePurples'}, - {name: 'Reds', value: 'interpolateReds'}, + {name: 'Blues', value: 'interpolateBlues', invert: 'dark'}, + {name: 'Greens', value: 'interpolateGreens', invert: 'dark'}, + {name: 'Greys', value: 'interpolateGreys', invert: 'dark'}, + {name: 'Oranges', value: 'interpolateOranges', invert: 'dark'}, + {name: 'Purples', value: 'interpolatePurples', invert: 'dark'}, + {name: 'Reds', value: 'interpolateReds', invert: 'dark'}, // Sequential (Multi-Hue) - {name: 'BuGn', value: 'interpolateBuGn'}, - {name: 'BuPu', value: 'interpolateBuPu'}, - {name: 'GnBu', value: 'interpolateGnBu'}, - {name: 'OrRd', value: 'interpolateOrRd'}, - {name: 'PuBuGn', value: 'interpolatePuBuGn'}, - {name: 'PuBu', value: 'interpolatePuBu'}, - {name: 'PuRd', value: 'interpolatePuRd'}, - {name: 'RdPu', value: 'interpolateRdPu'}, - {name: 'YlGnBu', value: 'interpolateYlGnBu'}, - {name: 'YlGn', value: 'interpolateYlGn'}, - {name: 'YlOrBr', value: 'interpolateYlOrBr'}, - {name: 'YlOrRd', value: 'interpolateYlOrRd'} + {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'} ]; export class HeatmapCtrl extends MetricsPanelCtrl { diff --git a/public/app/plugins/panel/heatmap/partials/display_editor.html b/public/app/plugins/panel/heatmap/partials/display_editor.html index 530a7ecbcf9..92409b3ad5e 100644 --- a/public/app/plugins/panel/heatmap/partials/display_editor.html +++ b/public/app/plugins/panel/heatmap/partials/display_editor.html @@ -2,7 +2,7 @@
Colors
- +
@@ -10,44 +10,38 @@
- +
- -
+ +
- +
- - +
-
+
- +
- - +
- - + +
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 93112dca496..12316e6be79 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; -import {appEvents} from 'app/core/core'; +import {appEvents, contextSrv} from 'app/core/core'; import d3 from 'd3'; import {HeatmapTooltip} from './heatmap_tooltip'; import {convertToCards, mergeZeroBuckets, removeZeroBuckets} from './heatmap_data_converter'; @@ -371,11 +371,11 @@ export default function link(scope, elem, attrs, ctrl) { return card.values.length; }); - setColorScale(max_value); + colorScale = getColorScale(max_value); setOpacityScale(max_value); setCardSize(); - if (panel.color.fillBackground && panel.color.mode === 'color') { + if (panel.color.fillBackground && panel.color.mode === 'spectrum') { fillBackground(heatmap, colorScale(0)); } @@ -426,9 +426,16 @@ export default function link(scope, elem, attrs, ctrl) { } } - function setColorScale(max_value) { - let colorInterpolator = d3[panel.color.colorScheme]; - colorScale = d3.scaleSequential(colorInterpolator).domain([max_value, 0]); + function getColorScale(maxValue) { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + let colorInterpolator = d3[colorScheme.value]; + let colorScaleInverted = colorScheme.invert === 'always' || + (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); + + let start = colorScaleInverted ? maxValue : 0; + let end = colorScaleInverted ? 0 : maxValue; + + return d3.scaleSequential(colorInterpolator).domain([start, end]); } function setOpacityScale(max_value) { @@ -710,8 +717,7 @@ export default function link(scope, elem, attrs, ctrl) { let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth()); let legendHeight = d3.select("#heatmap-color-legend").attr("height"); - let colorInterpolator = d3[panel.color.colorScheme]; - let legendColorScale = d3.scaleSequential(colorInterpolator).domain([legendWidth, 0]); + let legendColorScale = getColorScale(legendWidth); let rangeStep = 2; let valuesRange = d3.range(0, legendWidth, rangeStep); diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 1ba1bec9b83..3467c5b7542 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -30,6 +30,10 @@ describe('grafanaHeatmap', function () { beforeEach(angularMocks.inject(function ($rootScope, $compile) { var ctrl: any = { + colorSchemes: [ + {name: 'Oranges', value: 'interpolateOranges', invert: 'dark'}, + {name: 'Reds', value: 'interpolateReds', invert: 'dark'}, + ], events: new Emitter(), height: 200, panel: { @@ -40,11 +44,11 @@ describe('grafanaHeatmap', function () { cardRound: null }, color: { - mode: 'color', + mode: 'spectrum', cardColor: '#b4ff00', colorScale: 'linear', exponent: 0.5, - colorScheme: 'interpolateSpectral', + colorScheme: 'interpolateOranges', fillBackground: false }, xBucketSize: 1000, From ecd404335e6d111fd0e567d28547656de8fbf472 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 31 Mar 2017 19:13:26 +0400 Subject: [PATCH 24/37] heatmap: fix error with null values in ES converter (#7999) --- public/app/plugins/panel/heatmap/heatmap_data_converter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 800eb26f322..ba1cd223584 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -30,7 +30,12 @@ function convertEsSeriesToHeatmap(series: TimeSeries, saveZeroCounts = false) { _.forEach(series.datapoints, point => { let bound = series.alias; let count = point[VALUE_INDEX]; - let values = new Array(count); + + if (!count) { + return; + } + + let values = new Array(Math.round(count)); values.fill(Number(bound)); let valueBuckets = {}; From 4a2efb100782c3ae9696a8dfaa040b4eeec592b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 31 Mar 2017 23:07:57 +0200 Subject: [PATCH 25/37] alerting: removed width restriction on message, fixes #8000 --- public/app/features/alerting/partials/alert_tab.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 76841db75ba..4c63c4507fc 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -129,7 +129,7 @@
Message - +
From 95e0010a5be0cb1f3b7b43122a289b854ef648f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 3 Apr 2017 09:42:11 +0200 Subject: [PATCH 26/37] fix: sqlite3 and concurrent write requests caused database locked error some times, depending on how many concurrent requests and speed of disk drive. Fixed by reducign the number of concurrent db connections to 1 for sqlite3, fixes #7992 --- pkg/services/sqlstore/sqlstore.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index a2106ddbffd..75454b9d4d2 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -198,6 +198,9 @@ func LoadConfig() { if DbCfg.Type == "sqlite3" { UseSQLite3 = true + // only allow one connection as sqlite3 has multi threading issues that casue table locks + DbCfg.MaxIdleConn = 1 + DbCfg.MaxOpenConn = 1 } DbCfg.SslMode = sec.Key("ssl_mode").String() DbCfg.CaCertPath = sec.Key("ca_cert_path").String() From dd800fd14df122e61d8ea10eb67d42d616c8983b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 3 Apr 2017 22:07:09 +0200 Subject: [PATCH 27/37] fix: table panel could not show annotations due to bug introduced in 4.2, fixes #8023 --- CHANGELOG.md | 3 +++ public/app/plugins/panel/table/module.html | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a643338979a..8f2b6cfc187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ * **Oauth**: Add common type for oauth authorization errors [#6428](https://github.com/grafana/grafana/issues/6428) thx [@amenzhinsky](https://github.com/amenzhinsky) * **Templating**: Data source variable now supports multi value and panel repeats [#7030](https://github.com/grafana/grafana/issues/7030) thx [@mtanda](https://github.com/mtanda) +## Fixes +* **Table Panel**: Fixed annotation display in table panel, [#8023](https://github.com/grafana/grafana/issues/8023) + # 4.2.0 (2017-03-22) ## Minor Enhancements * **Templates**: Prevent use of the prefix `__` for templates in web UI [#7678](https://github.com/grafana/grafana/issues/7678) diff --git a/public/app/plugins/panel/table/module.html b/public/app/plugins/panel/table/module.html index 9140a182541..e405c6c42e3 100644 --- a/public/app/plugins/panel/table/module.html +++ b/public/app/plugins/panel/table/module.html @@ -1,7 +1,7 @@
-
-
+
+
@@ -21,10 +21,10 @@
-
- - No datapoints No datapoints returned from metric query - +
+ + No data to show Nothing returned by data query +
From 48c4e549f59b8ca0fce58f37ef5aa613413833a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 4 Apr 2017 09:28:33 +0200 Subject: [PATCH 28/37] Revert "support panel repeat for datasource template variable (#7711)" This reverts commit 554f972a25f483e22db5d74aeb3fd3a9cbe89129. --- public/app/core/services/datasource_srv.js | 4 ++-- public/app/features/panel/metrics_panel_ctrl.ts | 2 +- .../features/templating/datasource_variable.ts | 15 --------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/public/app/core/services/datasource_srv.js b/public/app/core/services/datasource_srv.js index bb187a7d5fb..e722aae355b 100644 --- a/public/app/core/services/datasource_srv.js +++ b/public/app/core/services/datasource_srv.js @@ -14,12 +14,12 @@ function (angular, _, coreModule, config) { this.datasources = {}; }; - this.get = function(name, scopedDsVars) { + this.get = function(name) { if (!name) { return this.get(config.defaultDatasource); } - name = templateSrv.replace(name, scopedDsVars || {}); + name = templateSrv.replace(name); if (name === 'default') { return this.get(config.defaultDatasource); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 6f066723741..b42d4a8b02a 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -92,7 +92,7 @@ class MetricsPanelCtrl extends PanelCtrl { // load datasource service this.setTimeQueryStart(); - this.datasourceSrv.get(this.panel.datasource, this.panel.scopedVars) + this.datasourceSrv.get(this.panel.datasource) .then(this.updateTimeRange.bind(this)) .then(this.issueQueries.bind(this)) .then(this.handleQueryResult.bind(this)) diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 66b0ab2eff9..41f2262ab4a 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -10,8 +10,6 @@ export class DatasourceVariable implements Variable { query: string; options: any; current: any; - multi: boolean; - includeAll: boolean; refresh: any; defaults = { @@ -23,8 +21,6 @@ export class DatasourceVariable implements Variable { regex: '', options: [], query: '', - multi: false, - includeAll: false, refresh: 1, }; @@ -75,16 +71,9 @@ export class DatasourceVariable implements Variable { } this.options = options; - if (this.includeAll) { - this.addAllOption(); - } return this.variableSrv.validateVariableSelectionState(this); } - addAllOption() { - this.options.unshift({text: 'All', value: "$__all"}); - } - dependsOn(variable) { if (this.regex) { return containsVariable(this.regex, variable.name); @@ -97,9 +86,6 @@ export class DatasourceVariable implements Variable { } getValueForUrl() { - if (this.current.text === 'All') { - return 'All'; - } return this.current.value; } } @@ -107,6 +93,5 @@ export class DatasourceVariable implements Variable { variableTypes['datasource'] = { name: 'Datasource', ctor: DatasourceVariable, - supportsMulti: true, description: 'Enabled you to dynamically switch the datasource for multiple panels', }; From 308d46e4ef86b3a67279394c94bad25ea6373b9a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 4 Apr 2017 13:02:25 +0400 Subject: [PATCH 29/37] Fix 8013 (#8027) * Add @ngInject to HeatmapCtrl constructor, issue #8013 * Remove unused loadPluginCss --- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 1 + public/app/plugins/panel/heatmap/module.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 2777b84e2f8..c9877a2b41c 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -99,6 +99,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { timeSrv: any; dataWarning: any; + /** @ngInject */ constructor($scope, $injector, private $rootScope, timeSrv) { super($scope, $injector); this.$rootScope = $rootScope; diff --git a/public/app/plugins/panel/heatmap/module.ts b/public/app/plugins/panel/heatmap/module.ts index 3144e335a39..d6926455563 100644 --- a/public/app/plugins/panel/heatmap/module.ts +++ b/public/app/plugins/panel/heatmap/module.ts @@ -1,6 +1,5 @@ /// -import {loadPluginCss} from 'app/plugins/sdk'; import {HeatmapCtrl} from './heatmap_ctrl'; export { From 2b81bfe9bf7019f8aa986ff716048d0aec102764 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 5 Apr 2017 19:17:55 +0400 Subject: [PATCH 30/37] alerting: fix $timeFilter cannot be used twice (#7969) (#8037) --- pkg/tsdb/influxdb/query.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 50d155131fd..19f374b938b 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -34,13 +34,18 @@ func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { return "", err } - res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), 1) - res = strings.Replace(res, "$interval", interval.Text, 1) - res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), 1) - res = strings.Replace(res, "$__interval", interval.Text, 1) + res = replaceVariable(res, "$timeFilter", query.renderTimeFilter(queryContext)) + res = replaceVariable(res, "$interval", interval.Text) + res = replaceVariable(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10)) + res = replaceVariable(res, "$__interval", interval.Text) return res, nil } +func replaceVariable(str string, variable string, value string) string { + count := strings.Count(str, variable) + return strings.Replace(str, variable, value, count) +} + func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) (*tsdb.Interval, error) { defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) From a79102b42dadb62f3de715779e5f4de86cfe850e Mon Sep 17 00:00:00 2001 From: Calle Pettersson Date: Thu, 6 Apr 2017 09:53:11 +0200 Subject: [PATCH 31/37] Fix ldap ca_cert example/docs for proper syntax (#8044) --- conf/ldap.toml | 2 +- docs/sources/installation/ldap.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index 305929b80d7..ae217106cb2 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -14,7 +14,7 @@ start_tls = false # set to true if you want to skip ssl cert validation ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults -# root_ca_cert = /path/to/certificate.crt +# root_ca_cert = "/path/to/certificate.crt" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index d8538182b68..e8e1c8e57bd 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -38,7 +38,7 @@ start_tls = false # set to true if you want to skip ssl cert validation ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults -# root_ca_cert = /path/to/certificate.crt +# root_ca_cert = "/path/to/certificate.crt" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" From f5f0d94ad40dec3443a51df0831b5afd2092ebdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Apr 2017 13:18:31 +0200 Subject: [PATCH 32/37] fix: temp/testing fix for hanging issue, #8033 --- pkg/services/sqlstore/sqlstore.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 75454b9d4d2..27fba0068d1 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -199,8 +199,8 @@ func LoadConfig() { if DbCfg.Type == "sqlite3" { UseSQLite3 = true // only allow one connection as sqlite3 has multi threading issues that casue table locks - DbCfg.MaxIdleConn = 1 - DbCfg.MaxOpenConn = 1 + // DbCfg.MaxIdleConn = 1 + // DbCfg.MaxOpenConn = 1 } DbCfg.SslMode = sec.Key("ssl_mode").String() DbCfg.CaCertPath = sec.Key("ca_cert_path").String() From c219498aab67a186f4b943388b541ff001bffebf Mon Sep 17 00:00:00 2001 From: wrp Date: Fri, 7 Apr 2017 00:29:46 -0700 Subject: [PATCH 33/37] Fix typo: "your" -> "your're" (#8064) --- docs/sources/plugins/developing/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/plugins/developing/development.md b/docs/sources/plugins/developing/development.md index 1f8db99424b..7d67dd22323 100644 --- a/docs/sources/plugins/developing/development.md +++ b/docs/sources/plugins/developing/development.md @@ -17,7 +17,7 @@ There are two blog posts about authoring a plugin that might also be of interest ## Short version 1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) -2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir) +2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if you're running development version from source dir) 3. Code away! ## What languages? From e6cc5df9d90dede2757bdbfdff402348e0dc00be Mon Sep 17 00:00:00 2001 From: tamayika Date: Fri, 7 Apr 2017 16:31:36 +0900 Subject: [PATCH 34/37] Fix restoration of ad-hoc variable value issue (#8057) * Fix restoration of ad-hoc variable value issue * Escape delimiters in ad-hoc variable URL * Remove unnecessary newline --- public/app/features/templating/adhoc_variable.ts | 16 ++++++++++++++-- .../templating/specs/adhoc_variable_specs.ts | 9 +++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index b3c9980c7c7..2cf4ccb2331 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -45,7 +45,9 @@ export class AdhocVariable implements Variable { } this.filters = urlValue.map(item => { - var values = item.split('|'); + var values = item.split('|').map(value => { + return this.unescapeDelimiter(value); + }); return { key: values[0], operator: values[1], @@ -58,10 +60,20 @@ export class AdhocVariable implements Variable { getValueForUrl() { return this.filters.map(filter => { - return filter.key + '|' + filter.operator + '|' + filter.value; + return [filter.key, filter.operator, filter.value].map(value => { + return this.escapeDelimiter(value); + }).join('|'); }); } + escapeDelimiter(value) { + return value.replace('|', '__gfpipe'); + } + + unescapeDelimiter(value) { + return value.replace('__gfpipe', '|'); + } + setFilters(filters: any[]) { this.filters = filters; } diff --git a/public/app/features/templating/specs/adhoc_variable_specs.ts b/public/app/features/templating/specs/adhoc_variable_specs.ts index 15856940540..662b5623641 100644 --- a/public/app/features/templating/specs/adhoc_variable_specs.ts +++ b/public/app/features/templating/specs/adhoc_variable_specs.ts @@ -11,10 +11,11 @@ describe('AdhocVariable', function() { filters: [ {key: 'key1', operator: '=', value: 'value1'}, {key: 'key2', operator: '!=', value: 'value2'}, + {key: 'key3', operator: '=', value: 'value3a|value3b'}, ] }); var urlValue = variable.getValueForUrl(); - expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2"]); + expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfpipevalue3b"]); }); }); @@ -23,7 +24,7 @@ describe('AdhocVariable', function() { it('should restore filters', function() { var variable = new AdhocVariable({}); - variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2"]); + variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfpipevalue3b"]); expect(variable.filters[0].key).to.be('key1'); expect(variable.filters[0].operator).to.be('='); @@ -32,6 +33,10 @@ describe('AdhocVariable', function() { expect(variable.filters[1].key).to.be('key2'); expect(variable.filters[1].operator).to.be('!='); expect(variable.filters[1].value).to.be('value2'); + + expect(variable.filters[2].key).to.be('key3'); + expect(variable.filters[2].operator).to.be('='); + expect(variable.filters[2].value).to.be('value3a|value3b'); }); }); From 7e14797b108c8ae505bdecb5a4194879c1ca8060 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 7 Apr 2017 12:07:30 +0400 Subject: [PATCH 35/37] graph: initial histogram support #600 (#8053) * graph: initial histogram support #600 * graph histogram mode: add Bars number option * graph histogram mode: fix X axis ticks calculation * graph histogram mode: change bar style (align and width) * refactor(graph): move histogram functions into separate module * graph histogram mode: rename series to "count" * graph histogram mode: fix errors if no data * refactor(graph and heatmap): move shared code into app/core * graph: add tests for histogram mode --- public/app/core/utils/ticks.ts | 27 ++++++++ .../app/plugins/panel/graph/axes_editor.html | 6 ++ public/app/plugins/panel/graph/axes_editor.ts | 1 + .../app/plugins/panel/graph/data_processor.ts | 13 ++++ public/app/plugins/panel/graph/graph.ts | 59 ++++++++++++++++- public/app/plugins/panel/graph/histogram.ts | 48 ++++++++++++++ public/app/plugins/panel/graph/module.ts | 1 + .../panel/graph/specs/histogram_specs.ts | 65 +++++++++++++++++++ public/app/plugins/panel/heatmap/rendering.ts | 24 +------ 9 files changed, 220 insertions(+), 24 deletions(-) create mode 100644 public/app/core/utils/ticks.ts create mode 100644 public/app/plugins/panel/graph/histogram.ts create mode 100644 public/app/plugins/panel/graph/specs/histogram_specs.ts diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts new file mode 100644 index 00000000000..7e7abbcd8f0 --- /dev/null +++ b/public/app/core/utils/ticks.ts @@ -0,0 +1,27 @@ +/** + * Calculate tick step. + * Implementation from d3-array (ticks.js) + * https://github.com/d3/d3-array/blob/master/src/ticks.js + * @param start Start value + * @param stop End value + * @param count Ticks count + */ +export function tickStep(start: number, stop: number, count: number): number { + let e10 = Math.sqrt(50), + e5 = Math.sqrt(10), + e2 = Math.sqrt(2); + + let step0 = Math.abs(stop - start) / Math.max(0, count), + step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), + error = step0 / step1; + + if (error >= e10) { + step1 *= 10; + } else if (error >= e5) { + step1 *= 5; + } else if (error >= e2) { + step1 *= 2; + } + + return stop < start ? -step1 : step1; +} diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index e446bb96490..0d30165817b 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -66,6 +66,12 @@
+ +
+ + +
+
diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index b73ddf3868e..adb3d1d6706 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -30,6 +30,7 @@ export class AxesEditorCtrl { this.xAxisModes = { 'Time': 'time', 'Series': 'series', + 'Histogram': 'histogram' // 'Data field': 'field', }; diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index dcd202c5bee..87b48f78550 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -29,6 +29,7 @@ export class DataProcessor { switch (this.panel.xaxis.mode) { case 'series': + case 'histogram': case 'time': { return options.dataList.map((item, index) => { return this.timeSeriesHandler(item, index, options); @@ -48,6 +49,9 @@ export class DataProcessor { if (this.panel.xaxis.mode === 'series') { return 'series'; } + if (this.panel.xaxis.mode === 'histogram') { + return 'histogram'; + } return 'time'; } } @@ -74,6 +78,15 @@ export class DataProcessor { this.panel.xaxis.values = ['total']; break; } + case 'histogram': { + this.panel.bars = true; + this.panel.lines = false; + this.panel.points = false; + this.panel.stack = false; + this.panel.legend.show = false; + this.panel.tooltip.shared = false; + break; + } } } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 06af66ddbe7..5a684a42fd3 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -12,10 +12,12 @@ import './jquery.flot.events'; import $ from 'jquery'; import _ from 'lodash'; import moment from 'moment'; -import kbn from 'app/core/utils/kbn'; +import kbn from 'app/core/utils/kbn'; +import {tickStep} from 'app/core/utils/ticks'; import {appEvents, coreModule} from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import {ThresholdManager} from './threshold_manager'; +import {convertValuesToHistogram, getSeriesValues} from './histogram'; coreModule.directive('grafanaGraph', function($rootScope, timeSrv) { return { @@ -290,6 +292,29 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) { addXSeriesAxis(options); break; } + case 'histogram': { + let bucketSize: number; + let values = getSeriesValues(data); + + if (data.length && values.length) { + let histMin = _.min(_.map(data, s => s.stats.min)); + let histMax = _.max(_.map(data, s => s.stats.max)); + let ticks = panel.xaxis.buckets || panelWidth / 50; + bucketSize = tickStep(histMin, histMax, ticks); + let histogram = convertValuesToHistogram(values, bucketSize); + + data[0].data = histogram; + data[0].alias = data[0].label = data[0].id = "count"; + data = [data[0]]; + + options.series.bars.barWidth = bucketSize * 0.8; + } else { + bucketSize = 0; + } + + addXHistogramAxis(options, bucketSize); + break; + } case 'table': { options.series.bars.barWidth = 0.7; options.series.bars.align = 'center'; @@ -384,6 +409,38 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) { }; } + function addXHistogramAxis(options, bucketSize) { + let ticks, min, max; + + if (data.length) { + ticks = _.map(data[0].data, point => point[0]); + + // Expand ticks for pretty view + min = Math.max(0, _.min(ticks) - bucketSize); + max = _.max(ticks) + bucketSize; + + ticks = []; + for (let i = min; i <= max; i += bucketSize) { + ticks.push(i); + } + } else { + // Set defaults if no data + ticks = panelWidth / 100; + min = 0; + max = 1; + } + + options.xaxis = { + timezone: dashboard.getTimezone(), + show: panel.xaxis.show, + mode: null, + min: min, + max: max, + label: "Histogram", + ticks: ticks + }; + } + function addXTableAxis(options) { var ticks = _.map(data, function(series, seriesIndex) { return _.map(series.datapoints, function(point, pointIndex) { diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts new file mode 100644 index 00000000000..e6ad7ebb0f6 --- /dev/null +++ b/public/app/plugins/panel/graph/histogram.ts @@ -0,0 +1,48 @@ +import _ from 'lodash'; + +/** + * Convert series into array of series values. + * @param data Array of series + */ +export function getSeriesValues(data: any): number[] { + let values = []; + + // Count histogam stats + for (let i = 0; i < data.length; i++) { + let series = data[i]; + for (let j = 0; j < series.data.length; j++) { + if (series.data[j][1] !== null) { + values.push(series.data[j][1]); + } + } + } + + return values; +} + +/** + * Convert array of values into timeseries-like histogram: + * [[val_1, count_1], [val_2, count_2], ..., [val_n, count_n]] + * @param values + * @param bucketSize + */ +export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { + let histogram = {}; + + for (let i = 0; i < values.length; i++) { + let bound = getBucketBound(values[i], bucketSize); + if (histogram[bound]) { + histogram[bound] = histogram[bound] + 1; + } else { + histogram[bound] = 1; + } + } + + return _.map(histogram, (count, bound) => { + return [Number(bound), count]; + }); +} + +function getBucketBound(value: number, bucketSize: number): number { + return Math.floor(value / bucketSize) * bucketSize; +} diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 01f7a485aa6..e98d1c25ad7 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -59,6 +59,7 @@ class GraphCtrl extends MetricsPanelCtrl { mode: 'time', name: null, values: [], + buckets: null }, // show/hide lines lines : true, diff --git a/public/app/plugins/panel/graph/specs/histogram_specs.ts b/public/app/plugins/panel/graph/specs/histogram_specs.ts new file mode 100644 index 00000000000..71b3def8d1d --- /dev/null +++ b/public/app/plugins/panel/graph/specs/histogram_specs.ts @@ -0,0 +1,65 @@ +/// + +import { describe, beforeEach, it, expect } from '../../../../../test/lib/common'; + +import { convertValuesToHistogram, getSeriesValues } from '../histogram'; + +describe('Graph Histogam Converter', function () { + + describe('Values to histogram converter', () => { + let values; + let bucketSize = 10; + + beforeEach(() => { + values = [1, 2, 10, 11, 17, 20, 29]; + }); + + it('Should convert to series-like array', () => { + bucketSize = 10; + let expected = [ + [0, 2], [10, 3], [20, 2] + ]; + + let histogram = convertValuesToHistogram(values, bucketSize); + expect(histogram).to.eql(expected); + }); + + it('Should not add empty buckets', () => { + bucketSize = 5; + let expected = [ + [0, 2], [10, 2], [15, 1], [20, 1], [25, 1] + ]; + + let histogram = convertValuesToHistogram(values, bucketSize); + expect(histogram).to.eql(expected); + }); + }); + + describe('Series to values converter', () => { + let data; + + beforeEach(() => { + data = [ + { + data: [[0, 1], [0, 2], [0, 10], [0, 11], [0, 17], [0, 20], [0, 29]] + } + ]; + }); + + it('Should convert to values array', () => { + let expected = [1, 2, 10, 11, 17, 20, 29]; + + let values = getSeriesValues(data); + expect(values).to.eql(expected); + }); + + it('Should skip null values', () => { + data[0].data.push([0, null]); + + let expected = [1, 2, 10, 11, 17, 20, 29]; + + let values = getSeriesValues(data); + expect(values).to.eql(expected); + }); + }); +}); diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 12316e6be79..a0f6786baf1 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -5,6 +5,7 @@ import $ from 'jquery'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import {appEvents, contextSrv} from 'app/core/core'; +import {tickStep} from 'app/core/utils/ticks'; import d3 from 'd3'; import {HeatmapTooltip} from './heatmap_tooltip'; import {convertToCards, mergeZeroBuckets, removeZeroBuckets} from './heatmap_data_converter'; @@ -836,29 +837,6 @@ function grafanaTimeFormat(ticks, min, max) { return "%H:%M"; } -// Calculate tick step. -// Implementation from d3-array (ticks.js) -// https://github.com/d3/d3-array/blob/master/src/ticks.js -function tickStep(start, stop, count) { - var e10 = Math.sqrt(50), - e5 = Math.sqrt(10), - e2 = Math.sqrt(2); - - var step0 = Math.abs(stop - start) / Math.max(0, count), - step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), - error = step0 / step1; - - if (error >= e10) { - step1 *= 10; - } else if (error >= e5) { - step1 *= 5; - } else if (error >= e2) { - step1 *= 2; - } - - return stop < start ? -step1 : step1; -} - function logp(value, base) { return Math.log(value) / Math.log(base); } From 7ec6e84fe80569913ea8b0e3eaee7c319be59c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Apr 2017 10:08:10 +0200 Subject: [PATCH 36/37] adhocfilters: minor escape change --- public/app/features/templating/adhoc_variable.ts | 4 ++-- public/app/features/templating/specs/adhoc_variable_specs.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index 2cf4ccb2331..f3ff80acfc8 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -67,11 +67,11 @@ export class AdhocVariable implements Variable { } escapeDelimiter(value) { - return value.replace('|', '__gfpipe'); + return value.replace('|', '__gfp__'); } unescapeDelimiter(value) { - return value.replace('__gfpipe', '|'); + return value.replace('__gfp__', '|'); } setFilters(filters: any[]) { diff --git a/public/app/features/templating/specs/adhoc_variable_specs.ts b/public/app/features/templating/specs/adhoc_variable_specs.ts index 662b5623641..c9246941309 100644 --- a/public/app/features/templating/specs/adhoc_variable_specs.ts +++ b/public/app/features/templating/specs/adhoc_variable_specs.ts @@ -15,7 +15,7 @@ describe('AdhocVariable', function() { ] }); var urlValue = variable.getValueForUrl(); - expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfpipevalue3b"]); + expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfp__value3b"]); }); }); @@ -24,7 +24,7 @@ describe('AdhocVariable', function() { it('should restore filters', function() { var variable = new AdhocVariable({}); - variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfpipevalue3b"]); + variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfp__value3b"]); expect(variable.filters[0].key).to.be('key1'); expect(variable.filters[0].operator).to.be('='); From 3b47efe70e59c3619566bcfc68c0cd0cc2f965cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 7 Apr 2017 10:16:57 +0200 Subject: [PATCH 37/37] graph: minor change to histogram buckets input field --- public/app/plugins/panel/graph/axes_editor.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 0d30165817b..b0ab759bf18 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -39,10 +39,10 @@
X-Axis
- +
- +
@@ -50,26 +50,26 @@
- +
- +
- +
- - + +