Merge branch 'master' into sql-proxy
This commit is contained in:
commit
26804d630f
48 files changed
+3422
-32
No files matched your search
@@ -15,6 +15,10 @@
|
||||
* **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)
|
||||
|
||||
## 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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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?
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "4.1.1",
|
||||
"testing": "4.1.1"
|
||||
"stable": "4.2.0",
|
||||
"testing": "4.2.0"
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export class User {
|
||||
orgRole: any;
|
||||
timezone: string;
|
||||
helpFlags1: number;
|
||||
lightTheme: boolean;
|
||||
|
||||
constructor() {
|
||||
if (config.bootData.user) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -129,7 +129,7 @@
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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('|', '__gfp__');
|
||||
}
|
||||
|
||||
unescapeDelimiter(value) {
|
||||
return value.replace('__gfp__', '|');
|
||||
}
|
||||
|
||||
setFilters(filters: any[]) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
@@ -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__gfp__value3b"]);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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__gfp__value3b"]);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Vendored
+5
@@ -67,3 +67,8 @@ declare module 'remarkable' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'd3' {
|
||||
var d3: any;
|
||||
export default d3;
|
||||
}
|
||||
@@ -39,10 +39,10 @@
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">X-Axis</h5>
|
||||
<gf-form-switch class="gf-form" label="Show" label-class="width-5" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Show" label-class="width-6" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Mode</label>
|
||||
<label class="gf-form-label width-6">Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisOptionChanged()"> </select>
|
||||
</div>
|
||||
@@ -50,22 +50,28 @@
|
||||
|
||||
<!-- Table mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
|
||||
<label class="gf-form-label width-5">Name</label>
|
||||
<label class="gf-form-label width-6">Name</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.name" get-options="ctrl.getDataFieldNames(false)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<!-- Series mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
|
||||
<label class="gf-form-label width-5">Value</label>
|
||||
<label class="gf-form-label width-6">Value</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.values[0]" get-options="ctrl.getDataFieldNames(true)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<!-- Series mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'series'">
|
||||
<label class="gf-form-label width-5">Value</label>
|
||||
<label class="gf-form-label width-6">Value</label>
|
||||
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<!-- Histogram mode -->
|
||||
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
|
||||
<label class="gf-form-label width-6">Buckets</label>
|
||||
<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur bs-tooltip="'Number of buckets'" data-placement="right">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -30,6 +30,7 @@ export class AxesEditorCtrl {
|
||||
this.xAxisModes = {
|
||||
'Time': 'time',
|
||||
'Series': 'series',
|
||||
'Histogram': 'histogram'
|
||||
// 'Data field': 'field',
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -59,6 +59,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
mode: 'time',
|
||||
name: null,
|
||||
values: [],
|
||||
buckets: null
|
||||
},
|
||||
// show/hide lines
|
||||
lines : true,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Whitespace-only changes.
@@ -0,0 +1,49 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export class AxesEditorCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
unitFormats: any;
|
||||
logScales: any;
|
||||
dataFormats: 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
|
||||
};
|
||||
|
||||
this.dataFormats = {
|
||||
'Timeseries': 'timeseries',
|
||||
'ES histogram': 'es_histogram'
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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, elasticHistogramToHeatmap, calculateBucketSize, 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: 'spectrum',
|
||||
cardColor: '#b4ff00',
|
||||
colorScale: 'sqrt',
|
||||
exponent: 0.5,
|
||||
colorScheme: 'interpolateOranges',
|
||||
fillBackground: false
|
||||
},
|
||||
dataFormat: 'timeseries',
|
||||
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', 'spectrum'];
|
||||
let opacityScales = ['linear', 'sqrt'];
|
||||
|
||||
// Schemes from d3-scale-chromatic
|
||||
// https://github.com/d3/d3-scale-chromatic
|
||||
let colorSchemes = [
|
||||
// Diverging
|
||||
{name: 'Spectral', value: 'interpolateSpectral', invert: 'always'},
|
||||
{name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always'},
|
||||
|
||||
// Sequential (Single Hue)
|
||||
{name: 'Blues', value: 'interpolateBlues', invert: 'dark'},
|
||||
{name: 'Greens', value: 'interpolateGreens', invert: 'dark'},
|
||||
{name: 'Greys', value: 'interpolateGreys', invert: 'dark'},
|
||||
{name: 'Oranges', value: 'interpolateOranges', invert: 'dark'},
|
||||
{name: 'Purples', value: 'interpolatePurples', invert: 'dark'},
|
||||
{name: 'Reds', value: 'interpolateReds', invert: 'dark'},
|
||||
|
||||
// Sequential (Multi-Hue)
|
||||
{name: 'BuGn', value: 'interpolateBuGn', invert: 'dark'},
|
||||
{name: 'BuPu', value: 'interpolateBuPu', invert: 'dark'},
|
||||
{name: 'GnBu', value: 'interpolateGnBu', invert: 'dark'},
|
||||
{name: 'OrRd', value: 'interpolateOrRd', invert: 'dark'},
|
||||
{name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark'},
|
||||
{name: 'PuBu', value: 'interpolatePuBu', invert: 'dark'},
|
||||
{name: 'PuRd', value: 'interpolatePuRd', invert: 'dark'},
|
||||
{name: 'RdPu', value: 'interpolateRdPu', invert: 'dark'},
|
||||
{name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark'},
|
||||
{name: 'YlGn', value: 'interpolateYlGn', invert: 'dark'},
|
||||
{name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark'},
|
||||
{name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm'}
|
||||
];
|
||||
|
||||
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;
|
||||
dataWarning: any;
|
||||
|
||||
/** @ngInject */
|
||||
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, heatmapStats, bucketsData;
|
||||
let logBase = this.panel.yAxis.logBase;
|
||||
|
||||
if (this.panel.dataFormat === 'es_histogram') {
|
||||
heatmapStats = this.parseHistogramSeries(this.series);
|
||||
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);
|
||||
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) {
|
||||
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.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();
|
||||
}
|
||||
|
||||
onDataError() {
|
||||
this.series = [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
seriesHandler(seriesData) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
|
||||
let VALUE_INDEX = 0;
|
||||
let TIME_INDEX = 1;
|
||||
|
||||
interface XBucket {
|
||||
x: number;
|
||||
buckets: any;
|
||||
}
|
||||
|
||||
interface YBucket {
|
||||
y: number;
|
||||
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];
|
||||
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
|
||||
let values = new Array(Math.round(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 with 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:
|
||||
* {
|
||||
* 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: [],
|
||||
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);
|
||||
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: any = {};
|
||||
_.forEach(seriesBuckets, (seriesBucket, index) => {
|
||||
if (index === 0) {
|
||||
mergedBuckets = seriesBucket;
|
||||
} else {
|
||||
_.forEach(seriesBucket, (xBucket, xBound) => {
|
||||
if (mergedBuckets[xBound]) {
|
||||
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);
|
||||
|
||||
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;
|
||||
if (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
|
||||
* @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
|
||||
* @param objB
|
||||
*/
|
||||
function isHeatmapDataEqual(objA: any, objB: any): boolean {
|
||||
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) {
|
||||
is_eql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values));
|
||||
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;
|
||||
}
|
||||
|
||||
function emptyXOR(foo: any, bar: any): boolean {
|
||||
return (_.isEmpty(foo) || _.isEmpty(bar)) && !(_.isEmpty(foo) && _.isEmpty(bar));
|
||||
}
|
||||
|
||||
export {
|
||||
convertToHeatMap,
|
||||
elasticHistogramToHeatmap,
|
||||
convertToCards,
|
||||
removeZeroBuckets,
|
||||
mergeZeroBuckets,
|
||||
getMinLog,
|
||||
getValueBucketBound,
|
||||
isHeatmapDataEqual,
|
||||
calculateBucketSize
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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.panel.tooltip.show || _.isEmpty(this.scope.ctrl.data.buckets)) { return; }
|
||||
|
||||
if (!this.tooltip) {
|
||||
this.add();
|
||||
this.move(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (!this.panel.tooltip.show) { return; }
|
||||
|
||||
this.move(e);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("class", "heatmap-tooltip graph-tooltip grafana-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 = `<div class="graph-tooltip-time">${time}</div>
|
||||
<div class="heatmap-histogram"></div>`;
|
||||
|
||||
if (yData) {
|
||||
boundBottom = valueFormatter(yData.bounds.bottom);
|
||||
boundTop = valueFormatter(yData.bounds.top);
|
||||
valuesNumber = yData.values.length;
|
||||
tooltipHtml += `<div>
|
||||
bucket: <b>${boundBottom} - ${boundTop}</b> <br>
|
||||
count: <b>${valuesNumber}</b> <br>
|
||||
</div>`;
|
||||
|
||||
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: <br>";
|
||||
_.forEach(seriesStat, (values, series) => {
|
||||
tooltipHtml += ` - ${series}: <b>${values}</b><br>`;
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="icn-heatmap-panel.svg"
|
||||
inkscape:version="0.92.1 unknown"><metadata
|
||||
id="metadata108"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs106" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2491"
|
||||
inkscape:window-height="1410"
|
||||
id="namedview104"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.44"
|
||||
inkscape:cx="37.431994"
|
||||
inkscape:cy="46.396264"
|
||||
inkscape:window-x="69"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><rect
|
||||
x="-0.017525015"
|
||||
y="33.438038"
|
||||
style="opacity:0.35714285;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69" /><path
|
||||
style="opacity:0.42857145;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.874036,24.263391 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 h -7.891949 -7.891949 z"
|
||||
id="path4883"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.79365079;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.69883,24.337252 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 H 41.590779 33.69883 Z"
|
||||
id="path4885"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.80952382;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.523624,24.337251 v -7.46822 h 7.891949 7.89195 v 7.46822 7.46822 h -7.89195 -7.891949 z"
|
||||
id="path4887"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.348418,24.167764 v -7.46822 h 7.891949 7.891949 v 7.46822 7.46822 h -7.891949 -7.891949 z"
|
||||
id="path4889"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.24603176;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.173218,24.279957 v -7.46822 h 7.891947 7.891956 v 7.46822 7.46822 h -7.891956 -7.891947 z"
|
||||
id="path4891"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.38158725;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,40.968612 v -7.46822 h 7.891949 7.891954 v 7.46822 7.468221 h -7.891954 -7.891949 z"
|
||||
id="path4893"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.75396824;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.377433,40.884464 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4895"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.94444442;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.528693,41.011582 v -7.46822 h 7.891949 7.89195 v 7.46822 7.468221 h -7.89195 -7.891949 z"
|
||||
id="path4897"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.53174606;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.679956,41.011587 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4899"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.64285715;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.831216,40.956187 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4901"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 0.04924114,57.615687 v -7.46822 H 7.8882241 15.727207 v 7.46822 7.468221 H 7.8882241 0.04924114 Z"
|
||||
id="path4905"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.884627,57.648974 v -7.46822 h 7.838984 7.838983 v 7.46822 7.468221 h -7.838983 -7.838984 z"
|
||||
id="path4907"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:1;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.390785,57.601163 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4913"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.29365079;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,57.657262 v -7.46822 h 7.891947 7.891946 v 7.46822 7.468221 h -7.891946 -7.891947 z"
|
||||
id="path4915"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.73015873;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.226177,74.345913 v -7.46822 h 7.891948 7.891955 v 7.46822 7.468221 h -7.891955 -7.891948 z"
|
||||
id="path4917"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.380199,74.317863 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4919"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.66666667;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.534214,74.360232 v -7.46822 h 7.891949 7.89195 v 7.46822 7.468221 h -7.89195 -7.891949 z"
|
||||
id="path4921"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.84920636;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.688232,74.360242 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4923"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.70634921;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 16.842256,74.341769 v -7.46822 h 7.891949 7.891949 v 7.46822 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4925"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M -0.00372516,74.325127 V 66.856906 H 7.8882239 15.780174 v 7.468221 7.46822 H 7.8882239 -0.00372516 Z"
|
||||
id="path4927"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.13492061;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 0.04924124,91.034564 V 83.566343 H 7.8882241 15.727207 v 7.468221 7.468221 H 7.8882241 0.04924114 Z"
|
||||
id="path4929"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.26190479;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 16.88187,91.034561 V 83.56634 h 7.838983 7.838984 v 7.468221 7.468224 h -7.838983 -7.838983 z"
|
||||
id="path4931"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.58730158;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.714496,91.034569 v -7.468221 h 7.891949 7.891949 v 7.468221 7.468216 h -7.891949 -7.891949 z"
|
||||
id="path4933"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.30158727;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 50.547126,91.034561 V 83.56634 h 7.891949 7.89195 v 7.468221 7.468224 h -7.89195 -7.891949 z"
|
||||
id="path4935"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.15873018;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.379756,91.034564 v -7.468221 h 7.891949 7.891949 v 7.468221 7.468221 h -7.891949 -7.891949 z"
|
||||
id="path4937"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.11904764;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 84.212376,91.034568 v -7.468221 h 7.891952 7.89195 v 7.468221 7.468217 h -7.89195 -7.891952 z"
|
||||
id="path4939"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.89682539;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 50.555398,57.68591 v -7.46822 h 7.838983 7.838983 v 7.46822 7.468221 h -7.838983 -7.838983 z"
|
||||
id="path4941"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:1;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 33.720011,57.685908 v -7.46822 h 7.838983 7.838983 v 7.46822 7.468221 h -7.838983 -7.838983 z"
|
||||
id="path4943"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.16666667;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 0.04924152,24.249783 V 16.728597 H 7.8882245 15.727207 v 7.521186 7.521186 H 7.8882245 0.04924152 Z"
|
||||
id="path4976"
|
||||
inkscape:connector-curvature="0" /><rect
|
||||
x="16.900255"
|
||||
y="0.10238234"
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69-5-7-3" /><rect
|
||||
x="84.304306"
|
||||
y="0.12308588"
|
||||
style="opacity:0.11904764;fill:#decd87;fill-opacity:1;stroke-width:0.70710677"
|
||||
width="15.8115"
|
||||
height="15.049"
|
||||
id="rect69-5-2-2-6" /><path
|
||||
style="opacity:0.3174603;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 33.751268,7.6629239 V 0.19470386 h 7.891949 7.891949 V 7.6629239 15.131142 h -7.891949 -7.891949 z"
|
||||
id="path4885-1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.43650794;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M 50.602281,7.6629315 V 0.19471149 h 7.891949 7.891951 V 7.6629315 15.13115 H 58.49423 50.602281 Z"
|
||||
id="path4887-2"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.73015873;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="m 67.453295,7.4510673 v -7.4682202 h 7.89195 7.89195 v 7.4682202 7.4682177 h -7.89195 -7.89195 z"
|
||||
id="path4889-9"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="opacity:0.15873018;fill:#decd87;fill-opacity:1;stroke-width:0.10593221"
|
||||
d="M -0.02566414,7.5403525 V 0.0191665 H 7.8133188 15.652302 v 7.521186 7.5211835 H 7.8133188 -0.02566414 Z"
|
||||
id="path4976-3"
|
||||
inkscape:connector-curvature="0" /></svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,12 @@
|
||||
<div class="heatmap-wrapper">
|
||||
<div class="heatmap-canvas-wrapper">
|
||||
|
||||
<div class="datapoints-warning" ng-if="ctrl.dataWarning">
|
||||
<span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
|
||||
</div>
|
||||
|
||||
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
||||
</div>
|
||||
<!-- <div class="graph-legend-wrapper" ng-if="ctrl.panel.legend.show" heatmap-legend></div> -->
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@@ -0,0 +1,7 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {HeatmapCtrl} from './heatmap_ctrl';
|
||||
|
||||
export {
|
||||
HeatmapCtrl as PanelCtrl
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Y Axis</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-5"
|
||||
label="Show"
|
||||
checked="ctrl.panel.yAxis.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Unit</label>
|
||||
<div class="gf-form-dropdown-typeahead max-width-15"
|
||||
ng-model="ctrl.panel.yAxis.format"
|
||||
dropdown-typeahead2="editor.unitFormats"
|
||||
dropdown-typeahead-on-select="editor.setUnitFormat($subItem)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Scale</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.yAxis.logBase" ng-options="v as k for (k, v) in editor.logScales" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-10">
|
||||
<label class="gf-form-label width-5">Y-Min</label>
|
||||
<input type="text" class="gf-form-input" placeholder="auto" empty-to-null ng-model="ctrl.panel.yAxis.min" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form max-width-10">
|
||||
<label class="gf-form-label width-5">Y-Max</label>
|
||||
<input type="text" class="gf-form-input" placeholder="auto" empty-to-null ng-model="ctrl.panel.yAxis.max" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Decimals</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Override automatic decimal precision for axis.'"
|
||||
ng-model="ctrl.panel.yAxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
<div ng-show="ctrl.panel.yAxis.logBase === 1">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Buckets</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Number of buckets for Y axis.'"
|
||||
ng-model="ctrl.panel.yBucketNumber" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Bucket Size</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Size of bucket. Has priority over Buckets option.'"
|
||||
ng-model="ctrl.panel.yBucketSize" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="ctrl.panel.yAxis.logBase !== 1">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Split Buckets</label>
|
||||
<input type="number" class="gf-form-input width-10" placeholder="1" data-placement="right"
|
||||
bs-tooltip="'For log scales only. By default Y values is splitted by integer powers of log base (1, 2, 4, 8, 16, ... for log2). This option allows to split each default bucket into specified number of buckets.'"
|
||||
ng-model="ctrl.panel.yAxis.splitFactor" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-10"
|
||||
label="Remove zero values"
|
||||
checked="ctrl.panel.yAxis.removeZeroValues" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">X Axis</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Show"
|
||||
checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Buckets</label>
|
||||
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Number of buckets for X axis.'"
|
||||
ng-model="ctrl.panel.xBucketNumber" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Bucket Size</label>
|
||||
<input type="text" class="gf-form-input width-8" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Size of bucket. Number or interval (10s, 5m, 1h, etc). Supported intervals: ms, s, m, h, d, w, M, y. Has priority over Buckets option.'"
|
||||
ng-model="ctrl.panel.xBucketSize" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Data format</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Format</label>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.dataFormat" ng-options="v as k for (k, v) in editor.dataFormats" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Colors</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Mode</label>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.mode" ng-options="s for s in ctrl.colorModes" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ctrl.panel.color.mode === 'opacity'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.color.cardColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Scale</label>
|
||||
<div class="gf-form-select-wrapper width-8">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScale" ng-options="s for s in ctrl.opacityScales" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.panel.color.colorScale === 'sqrt'">
|
||||
<label class="gf-form-label width-7">Exponent</label>
|
||||
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<svg id="heatmap-opacity-legend" width="22.7em" height="2em"></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="ctrl.panel.color.mode === 'spectrum'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Scheme</label>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<svg id="heatmap-color-legend" width="22.7em" height="2em"></svg>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-9" label="Fill background" checked="ctrl.panel.color.fillBackground" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Cards</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Space</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardPadding" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Round</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Tooltip</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Show tooltip"
|
||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div ng-if="ctrl.panel.tooltip.show">
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Highlight cards"
|
||||
checked="ctrl.panel.highlightCards" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Series stats"
|
||||
checked="ctrl.panel.tooltip.seriesStat" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-8"
|
||||
label="Histogram"
|
||||
checked="ctrl.panel.tooltip.showHistogram" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Decimals</label>
|
||||
<input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right"
|
||||
bs-tooltip="'Max decimal precision for tooltip.'"
|
||||
ng-model="ctrl.panel.tooltipDecimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
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';
|
||||
|
||||
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 = 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;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
colorScale = getColorScale(max_value);
|
||||
setOpacityScale(max_value);
|
||||
setCardSize();
|
||||
|
||||
if (panel.color.fillBackground && panel.color.mode === 'spectrum') {
|
||||
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 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) {
|
||||
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 = data.yBucketSize || 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 legendColorScale = getColorScale(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() {
|
||||
data = ctrl.data;
|
||||
panel = ctrl.panel;
|
||||
timeRange = ctrl.range;
|
||||
|
||||
if (setElementHeight()) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, 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];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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 = {};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
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 = {
|
||||
colorSchemes: [
|
||||
{name: 'Oranges', value: 'interpolateOranges', invert: 'dark'},
|
||||
{name: 'Reds', value: 'interpolateReds', invert: 'dark'},
|
||||
],
|
||||
events: new Emitter(),
|
||||
height: 200,
|
||||
panel: {
|
||||
heatmap: {
|
||||
},
|
||||
cards: {
|
||||
cardPadding: null,
|
||||
cardRound: null
|
||||
},
|
||||
color: {
|
||||
mode: 'spectrum',
|
||||
cardColor: '#b4ff00',
|
||||
colorScale: 'linear',
|
||||
exponent: 0.5,
|
||||
colorScheme: 'interpolateOranges',
|
||||
fillBackground: false
|
||||
},
|
||||
xBucketSize: 1000,
|
||||
xBucketNumber: null,
|
||||
yBucketSize: 1,
|
||||
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('utc')
|
||||
},
|
||||
range: {
|
||||
from: moment.utc("01 Mar 2017 10:00:00"),
|
||||
to: moment.utc("01 Mar 2017 11:00:00"),
|
||||
},
|
||||
};
|
||||
|
||||
var scope = $rootScope.$new();
|
||||
scope.ctrl = ctrl;
|
||||
|
||||
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.data = {
|
||||
heatmapStats: {
|
||||
min: 1,
|
||||
max: 3,
|
||||
minLog: 1
|
||||
},
|
||||
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 = `
|
||||
<div class="heatmap-wrapper">
|
||||
<div class="heatmap-canvas-wrapper">
|
||||
<div class="heatmap-panel" style='width:${elementWidth}px'></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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.yAxis.logBase = 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");
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function getTicks(element, axisSelector) {
|
||||
return element.find(axisSelector).find("text")
|
||||
.map(function () {
|
||||
return this.textContent;
|
||||
}).get();
|
||||
}
|
||||
|
||||
function formatLocalTime(timeStr) {
|
||||
let format = "HH:mm";
|
||||
return moment.utc(timeStr).local().format(format);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
<div class="table-panel-container">
|
||||
<div class="table-panel-header-bg" ng-show="ctrl.dataRaw.length>0"></div>
|
||||
<div class="table-panel-scroll" ng-show="ctrl.dataRaw.length>0">
|
||||
<div class="table-panel-header-bg" ng-show="ctrl.table.rows.length"></div>
|
||||
<div class="table-panel-scroll" ng-show="ctrl.table.rows.length">
|
||||
<table class="table-panel-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -21,10 +21,10 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="datapoints-warning" ng-show="ctrl.dataRaw.length===0">
|
||||
<span class="small" >
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<div class="datapoints-warning" ng-show="ctrl.table.rows.length===0">
|
||||
<span class="small" >
|
||||
No data to show <tip>Nothing returned by data query</tip>
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-panel-footer">
|
||||
</div>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.heatmap-canvas-wrapper {
|
||||
// position: relative;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.heatmap-panel {
|
||||
position: relative;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Vendored
+27
@@ -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.
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
# D3: Data-Driven Documents
|
||||
|
||||
<a href="https://d3js.org"><img src="https://d3js.org/logo.svg" align="left" hspace="10" vspace="6"></a>
|
||||
|
||||
**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
|
||||
<script src="https://d3js.org/d3.v4.js"></script>
|
||||
```
|
||||
|
||||
For the minified version:
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3.v4.min.js"></script>
|
||||
```
|
||||
|
||||
You can also use the standalone D3 microlibraries. For example, [d3-selection](https://github.com/d3/d3-selection):
|
||||
|
||||
```html
|
||||
<script src="https://d3js.org/d3-selection.v1.js"></script>
|
||||
```
|
||||
|
||||
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"));
|
||||
```
|
||||
+2
@@ -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<e;)d[c]="#"+f.slice(6*c,6*++c);return d},c=d("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666"),a=d("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666"),b=d("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928"),r=d("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2"),n=d("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc"),t=d("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999"),o=d("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3"),p=d("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f"),m=function(f){return e.interpolateRgbBasis(f[f.length-1])},s=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(d),l=m(s),i=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(d),u=m(i),h=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(d),y=m(h),A=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(d),w=m(A),B=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(d),G=m(B),P=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(d),R=m(P),Y=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(d),O=m(Y),g=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(d),S=m(g),v=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(d),x=m(v),j=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccecLine truncated
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
// Import main D3.js module and combine it with another
|
||||
var d3 = Object.assign({}, require('./d3.v4.min.js'), require('./d3-scale-chromatic.min.js'));
|
||||
module.exports = d3;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
// https://d3js.org Version 4.5.0. Copyright 2017 Mike Bostock.
|
||||
(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t){return function(n,e){return Ns(t(n),e)}}function e(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Ys?i*=10:o>=Bs?i*=5:o>=js&&(i*=2),n<t?-i:i}function r(t){return t.length}function i(t,n,e){var r=t(e);return"translate("+(isFinite(r)?r:n(e))+",0)"}function o(t,n,e){var r=t(e);return"translate(0,"+(isFinite(r)?r:n(e))+")"}function u(t){var n=t.bandwidth()/2;return t.round()&&(n=Math.round(n)),function(e){return t(e)+n}}function a(){return!this.__axis}function c(t,n){function e(e){var p,d=null==c?n.ticks?n.ticks.apply(n,r):n.domain():c,v=null==s?n.tickFormat?n.tickFormat.apply(n,r):ff:s,_=Math.max(f,0)+h,y=t===lf||t===pf?i:o,g=n.range(),m=g[0]+.5,x=g[g.length-1]+.5,b=(n.bandwidth?u:ff)(n.copy()),w=e.selection?e.selection():e,M=w.selectAll(".domain").data([null]),T=w.selectAll(".tick").data(d,n).order(),N=T.exit(),k=T.enter().append("g").attr("class","tick"),S=T.select("line"),E=T.select("text"),A=t===lf||t===df?-1:1,C=t===df||t===hf?(p="x","y"):(p="y","x");M=M.merge(M.enter().insert("path",".tick").attr("class","domain").attr("stroke","#000")),T=T.merge(k),S=S.merge(k.append("line").attr("stroke","#000").attr(p+"2",A*f).attr(C+"1",.5).attr(C+"2",.5)),E=E.merge(k.append("text").attr("fill","#000").attr(p,A*_).attr(C,.5).attr("dy",t===lf?"0em":t===pf?"0.71em":"0.32em")),e!==w&&(M=M.transition(e),T=T.transition(e),S=S.transition(e),E=E.transition(e),N=N.transition(e).attr("opacity",vf).attr("transform",function(t){return y(b,this.parentNode.__axis||b,t)}),k.attr("opacity",vf).attr("transform",function(t){return y(this.parentNode.__axis||b,b,t)})),N.remove(),M.attr("d",t===df||t==hf?"M"+A*l+","+m+"H0.5V"+x+"H"+A*l:"M"+m+","+A*l+"V0.5H"+x+"V"+A*l),T.attr("opacity",1).attr("transform",function(t){return y(b,b,t)}),S.attr(p+"2",A*f),E.attr(p,A*_).text(v),w.filter(a).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===hf?"start":t===df?"end":"middle"),w.each(function(){this.__axis=b})}var r=[],c=null,s=null,f=6,l=6,h=3;return e.scale=function(t){return arguments.length?(n=t,e):n},e.ticks=function(){return r=sf.call(arguments),e},e.tickArguments=function(t){return arguments.length?(r=null==t?[]:sf.call(t),e):r.slice()},e.tickValues=function(t){return arguments.length?(c=null==t?null:sf.call(t),e):c&&c.slice()},e.tickFormat=function(t){return arguments.length?(s=t,e):s},e.tickSize=function(t){return arguments.length?(f=l=+t,e):f},e.tickSizeInner=function(t){return arguments.length?(f=+t,e):f},e.tickSizeOuter=function(t){return arguments.length?(l=+t,e):l},e.tickPadding=function(t){return arguments.length?(h=+t,e):h},e}function s(t){return c(lf,t)}function f(t){return c(hf,t)}function l(t){return c(pf,t)}function h(t){return c(df,t)}function p(){for(var t,n=0,e=arguments.length,r={};n<e;++n){if(!(t=arguments[n]+"")||t in r)throw new Error("illegal type: "+t);r[t]=[]}return new d(r)}function d(t){this._=t}function v(t,n){return t.trim().split(/^|\s+/).map(function(t){var e="",r=t.indexOf(".");if(r>=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<i;++r)if((e=t[r]).name===n)return e.value}function y(t,n,e){for(var r=0,i=t.length;r<i;++r)if(t[r].name===n){t[r]=_f,t=t.slice(0,r).concat(t.slice(r+1));break}return null!=e&&t.push({name:n,value:e}),t}function g(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===yf&&n.documentElement.namespaceURI===yf?n.createElement(t):n.createElementNS(e,t)}}function m(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function x(){return new b}function b(){this._="@"+(++bf).toString(36)}function w(t,n,e){return t=M(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function M(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function T(t){return t.trim().split(/^|\s+/).map(function(t){var n="",e=t.indexOf(".");return e>=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;r<o;++r)e=n[r],t.type&&e.type!==t.type||e.name!==t.name?n[++i]=e:this.removeEventListener(e.type,e.listener,e.capture);++i?n.length=i:delete this.__on}}}function k(t,n,e){var r=kf.hasOwnProperty(t.type)?w:M;return function(i,o,u){var a,c=this.__on,s=r(n,o,u);if(c)for(var f=0,l=c.length;f<l;++f)if((a=c[f]).type===t.type&&a.name===t.name)return this.removeEventListener(a.type,a.listener,a.capture),this.addEventListener(a.type,a.listener=s,a.capture=e),void(a.value=n);this.addEventListener(t.type,s,e),a={type:t.type,name:t.name,value:n,listener:s,capture:e},c?c.push(a):tLine truncated
|
||||
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<o;)r=t[i],n.point(r[0],r[1],r[2]);n.lineEnd()}function br(t,n){var e=-1,r=t.length;for(n.polygonStart();++e<r;)xr(t[e],n,1);n.polygonEnd()}function wr(){B_.point=Tr}function Mr(){Nr(Fv,Iv)}function Tr(t,n){B_.point=Nr,Fv=t,Iv=n,t*=T_,n*=T_,Yv=t,Bv=E_(n=n/2+b_),jv=R_(n)}function Nr(t,n){t*=T_,n*=T_,n=n/2+b_;var e=t-Yv,r=e>=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]),n<Xv&&(Xv=n),n>Wv&&(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*$v<s&&s<c*t)?(u=o[1]*M_,u>Wv&&(Wv=u)):(s=(s+360)%360-180,f^(c*$v<s&&s<c*t)?(u=-o[1]*M_,u<Xv&&(Xv=u)):(n<Xv&&(Xv=n),n>Wv&&(Wv=n))),f?t<$v?Ir(Hv,t)>Ir(Hv,Vv)&&(Vv=t):Ir(t,Vv)>Ir(Hv,Vv)&&(Hv=t):Vv>=Hv?(t<Hv&&(Hv=t),t>Vv&&(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]:n<t[0]||t[1]<n}function jr(t,n){t*=T_,n*=T_;var e=E_(n);Hr(e*E_(t),e*R_(t),R_(n))}function Hr(t,n,e){++t_,e_+=(t-e_)/t_,r_+=(n-r_)/t_,i_+=(e-i_)/t_}function Xr(){W_.point=Vr}function Vr(t,n){t*=T_,n*=T_;var e=E_(n);p_=e*E_(t),d_=e*R_(t),v_=R_(n),W_.point=Wr,Hr(p_,d_,v_)}function Wr(t,n){t*=T_,n*=T_;var e=E_(n),r=e*E_(t),i=e*R_(t),o=R_(n),u=S_(L_((u=d_*o-v_*i)*u+(u=v_*r-p_*o)*u+(u=p_*i-d_*r)*u),p_*r+d_*i+v_*o);n_+=u,o_+=u*(p_+(p_=r)),u_+=u*(d_+(d_=i)),a_+=u*(v_+(v_=o)),Hr(p_,d_,v_)}function $r(){W_.point=jr}function Zr(){W_.point=Jr}function Gr(){Qr(l_,h_),W_.point=jr}function Jr(t,n){l_=t,h_=n,t*=T_,n*=T_,W_.point=Qr;var e=E_(n);p_=e*E_(t),d_=e*R_(t),v_=R_(n),Hr(p_,d_,v_)}function Qr(t,n){t*=T_,n*=T_;var e=E_(n),r=e*E_(t),i=e*R_(t),o=R_(n),u=d_*o-v_*i,a=v_*r-p_*o,c=p_*i-d_*r,s=L_(u*u+a*a+c*c),f=p_*r+d_*i+v_*o,l=s&&-vr(f)/s,h=S_(s,f);c_+=l*u,s_+=l*a,f_+=l*c,n_+=h,o_+=h*(p_+(p_=r)),u_+=h*(d_+(d_=i)),a_+=h*(v_+(v_=o)),Hr(p_,d_,v_)}function Kr(t,n){return[t>m_?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?i<o:i>o)&&(i+=r*w_));for(var s,f=i;r>0?f>o:f<o;f-=c)s=kr([u,-a*E_(f),-a*R_(f)]),t.point(s[0],s[1])}}function oi(t,n){n=Sr(n),n[0]-=t,Pr(n);var e=vr(-n[1]);return((-n[2]<0?-e:e)+w_-y_)%w_}function ui(t,n,e,r){this.x=t,this.z=n,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function ai(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r<n;)i.n=e=t[r],e.p=i,i=e;i.n=e=t[0],e.p=i}}function ci(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,a,s){var f=0,l=0;if(null==i||(f=u(i,a))!==(l=u(o,a))||c(i,o)<0^a>0){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)<y_?i>0?0:3:N_(r[0]-e)<y_?i>0?2:1:N_(r[1]-n)<y_?i>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;e<i;++e)for(var o,u,a=_[e],c=1,s=a.length,f=a[0],l=f[0],h=f[1];c<s;++c)o=l,u=h,f=a[c],l=f[0],h=f[1],u<=r?h>r&&(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=Line truncated
|
||||
}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+(o<e?new Array(e-o+1).join(n)+i:i)}function Zu(t){return t.replace(Lx,"\\$&")}function Gu(t){return new RegExp("^(?:"+t.map(Zu).join("|")+")","i")}function Ju(t){for(var n={},e=-1,r=t.length;++e<r;)n[t[e].toLowerCase()]=e;return n}function Qu(t,n,e){var r=Rx.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function Ku(t,n,e){var r=Rx.exec(n.slice(e));return r?(t.U=+r[0],e+r[0].length):-1}function ta(t,n,e){var r=Rx.exec(n.slice(e));return r?(t.W=+r[0],e+r[0].length):-1}function na(t,n,e){var r=Rx.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function ea(t,n,e){var r=Rx.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?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){retLine truncated
|
||||
n):d},n.on=function(){var t=_.on.apply(_,arguments);return t===_?n:t},n},bl=function(t,n,e){t.prototype=n.prototype=e,e.constructor=t},wl=.7,Ml=1/wl,Tl="\\s*([+-]?\\d+)\\s*",Nl="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",kl="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",Sl=/^#([0-9a-f]{3})$/,El=/^#([0-9a-f]{6})$/,Al=new RegExp("^rgb\\("+[Tl,Tl,Tl]+"\\)$"),Cl=new RegExp("^rgb\\("+[kl,kl,kl]+"\\)$"),zl=new RegExp("^rgba\\("+[Tl,Tl,Tl,Nl]+"\\)$"),Pl=new RegExp("^rgba\\("+[kl,kl,kl,Nl]+"\\)$"),Rl=new RegExp("^hsl\\("+[Nl,kl,kl]+"\\)$"),ql=new RegExp("^hsla\\("+[Nl,kl,kl,Nl]+"\\)$"),Ll={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};bl(wt,Mt,{displayable:function(){return this.rgb().displayable()},toString:function(){return this.rgb()+""}}),bl(Et,St,bt(wt,{brighter:function(t){return t=null==t?Ml:Math.pow(Ml,t),new Et(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?wl:Math.pow(wl,t),new Et(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return 0<=this.r&&this.r<=255&&0<=this.g&&this.g<=255&&0<=this.b&&this.b<=255&&0<=this.opacity&&this.opacity<=1},toString:function(){var t=this.opacity;return t=isNaN(t)?1:Math.max(0,Math.min(1,t)),(1===t?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),bl(Pt,zt,bt(wt,{brighter:function(t){return t=null==t?Ml:Math.pow(Ml,t),new Pt(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?wl:Math.pow(wl,t),new Pt(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new Et(Rt(t>=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=isNLine truncated
|
||||
return arguments.length?(s=+t,o):s},velocityDecay:function(t){return arguments.length?(f=1-t,o):1-f},force:function(t,n){return arguments.length>1?(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;s<f;++s)a=t[s],i=n-a.x,o=e-a.y,u=i*i+o*o,u<r&&(c=a,r=u);return c},on:function(t,n){return arguments.length>1?(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<a;++n)o=i[n],c.visit(r)}function n(){if(i){var t,n,e=i.length;for(a=new Array(e),t=0;t<e;++t)n=i[t],a[n.index]=+c(n,t,i)}}function e(t){var n,e,r,i,o,u=0;if(t.length){for(r=i=o=0;o<4;++o)(n=t[o])&&(e=n.value)&&(u+=e,r+=e*n.x,i+=e*n.y);t.x=r/u,t.y=i/u}else{n=t,n.x=n.data.x,n.y=n.data.y;do u+=a[n.data.index];while(n=n.next)}t.value=u}function r(t,n,e,r){if(!t.value)return!0;var i=t.x-o.x,c=t.y-o.y,h=r-n,p=i*i+c*c;if(h*h/l<p)return p<f&&(0===i&&(i=Kd(),p+=i*i),0===c&&(c=Kd(),p+=c*c),p<s&&(p=Math.sqrt(s*p)),o.vx+=i*t.value*u/p,o.vy+=c*t.value*u/p),!0;if(!(t.length||p>=f)){(t.data!==o||t.next)&&(0===i&&(i=Kd(),p+=i*i),0===c&&(c=Kd(),p+=c*c),p<s&&(p=Math.sqrt(s*p)));do t.data!==o&&(h=a[t.data.index]*u/p,o.vx+=i*h,o.vy+=c*h);while(t=t.next)}}var i,o,u,a,c=Qd(-30),s=1,f=1/0,l=.81;return t.initialize=function(t){i=t,n()},t.strength=function(e){return arguments.length?(c="function"==typeof e?e:Qd(+e),n(),t):c},t.distanceMin=function(n){return arguments.length?(s=n*n,t):Math.sqrt(s)},t.distanceMax=function(n){return arguments.length?(f=n*n,t):Math.sqrt(f)},t.theta=function(n){return arguments.length?(l=n*n,t):Math.sqrt(l)},t},bv=function(t){function n(t){for(var n,e=0,u=r.length;e<u;++e)n=r[e],n.vx+=(o[e]-n.x)*i[e]*t}function e(){if(r){var n,e=r.length;for(i=new Array(e),o=new Array(e),n=0;n<e;++n)i[n]=isNaN(o[n]=+t(r[n],n,r))?0:+u(r[n],n,r)}}var r,i,o,u=Qd(.1);return"function"!=typeof t&&(t=Qd(null==t?0:+t)),n.initialize=function(t){r=t,e()},n.strength=function(t){return arguments.length?(u="function"==typeof t?t:Qd(+t),e(),n):u},n.x=function(r){return arguments.length?(t="function"==typeof r?r:Qd(+r),e(),n):t},n},wv=function(t){function n(t){for(var n,e=0,u=r.length;e<u;++e)n=r[e],n.vy+=(o[e]-n.y)*i[e]*t}function e(){if(r){var n,e=r.length;for(i=new Array(e),o=new Array(e),n=0;n<e;++n)i[n]=isNaN(o[n]=+t(r[n],n,r))?0:+u(r[n],n,r)}}var r,i,o,u=Qd(.1);return"function"!=typeof t&&(t=Qd(null==t?0:+t)),n.initialize=function(t){r=t,e()},n.strength=function(t){return arguments.length?(u="function"==typeof t?t:Qd(+t),e(),n):u},n.y=function(r){return arguments.length?(t="function"==typeof r?r:Qd(+r),e(),n):t},n},Mv=function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,r=t.slice(0,e);return[r.length>1?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;i<r;++i)switch(t[i]){case".":o=e=i;break;case"0":0===o&&(o=i),e=i;break;case"e":break t;default:o>0&&(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;++n<i;)if(c=t.charCodeAt(n),48<c&&c<58||"x"===p&&96<c&&c<103||"X"===p&&64<c&&c<71){x=!0;break}if(g=(x?"("===a?a:"-":"-"===a||"("===a?"":a)+g,m=m+("s"===p?Rv[8+dv/3]:Line truncated
|
||||
return function(){for(var n=0,e=0;e<t;++e)n+=Math.random();return n}},um=function(t){var n=om(t);return function(){return n()/t}},am=function(t){return function(){return-Math.log(1-Math.random())/t}},cm=function(t,n){function e(t){var n,e=s.status;if(!e&&hu(s)||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 u<o&&(e=r,r=i,i=e,e=o,o=u,u=e),t[r]=n.floor(o),t[i]=n.ceil(u),t},km=new Date,Sm=new Date,Em=Yu(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Em.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?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)Line truncated
|
||||
},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<e;++o,++u)this._context.bezierCurveTo(r[0][o],i[0][o],r[1][o],i[1][o],t[u],n[u]);(this._line||0!==this._line&&1===e)&&this._context.closePath(),this._line=1-this._line,this._x=this._y=null},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var nw=function(t){return new Pc(t)};qc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=this._y=NaN,this._point=0},lineEnd:function(){0<this._t&&this._t<1&&2===this._point&&this._context.lineTo(this._x,this._y),(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line>=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<r;++i){e=o,o=t[n[i]];for(var a=0;a<u;++a)o[a][1]+=o[a][0]=isNaN(e[a][1])?e[a][0]:e[a][1]}},ow=function(t){for(var n=t.length,e=new Array(n);--n>=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;o<s;++o){for(var l,h=a[o],p=f[o]=new Array(c),d=0;d<c;++d)p[d]=l=[0,+i(t[d],h,d,t)],l.data=t[d];p.key=h}for(o=0,u=e(f);o<s;++o)f[u[o]].index=o;return r(f,u),f}var n=cb([]),e=ow,r=iw,i=Dc;return t.keys=function(e){return arguments.length?(n="function"==typeof e?e:cb(rw.call(e)),t):n},t.value=function(n){return arguments.length?(i="function"==typeof n?n:cb(+n),t):i},t.order=function(n){return arguments.length?(e=null==n?ow:"function"==typeof n?n:cb(rw.call(n)),t):e},t.offset=function(n){return arguments.length?(r=null==n?iw:n,t):r},t},aw=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,u=t[0].length;o<u;++o){for(i=e=0;e<r;++e)i+=t[e][o][1]||0;if(i)for(e=0;e<r;++e)t[e][o][1]/=i}iw(t,n)}},cw=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r<o;++r){for(var u=0,a=0;u<e;++u)a+=t[u][r][1]||0;i[r][1]+=i[r][0]=-a/2}iw(t,n)}},sw=function(t,n){if((i=t.length)>0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u<r;++u){for(var a=0,c=0,s=0;a<i;++a){for(var f=t[n[a]],l=f[u][1]||0,h=f[u-1][1]||0,p=(l-Line truncated
|
||||
Reference in new issue
Block a user