Commit ecba23e8 by bergquist

Merge branch 'master' into influxdb_alias_seriename

parents f0a0e647 70b9ba25
# 4.0-pre (unreleased)
# 4.0-beta2 (unrelased)
### Bugfixes
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
# 4.0-beta1 (2016-11-09)
### Enhancements
* **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674)
......@@ -18,12 +23,13 @@
* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172)
* **Dashboard**: After inactivity hide nav/row actions, fade to nice clean view, can be toggled with `d v`, also added kiosk mode, toggled via `d k` [#6476](https://github.com/grafana/grafana/issues/6476)
* **Dashboard**: Improved dashboard row menu & add panel UX [#6442](https://github.com/grafana/grafana/issues/6442)
* **Graph Panel**: Support for stacking null values [#2912](https://github.com/grafana/grafana/issues/2912), [#6287](https://github.com/grafana/grafana/issues/6287), thanks @benrubson!
### Breaking changes
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
* **lodash upgrade**: Upgraded lodash from 2.4.2 to 4.15.0, this contains a number of breaking changes that could effect plugins. closes [#6021](https://github.com/grafana/grafana/pull/6021)
### Bugfixes
### Bug fixes
* **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999)
* **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467)
* **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070)
......
......@@ -5,7 +5,7 @@ machine:
GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana"
GODIST: "go1.7.1.linux-amd64.tar.gz"
GODIST: "go1.7.3.linux-amd64.tar.gz"
post:
- mkdir -p download
- test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
......
......@@ -25,31 +25,12 @@ to add and configure a `notification` object. This is done from the Alerting/Not
On the notifications list page hit the `New Notification` button to go the the page where you
can configure and setup a new notification.
You you specify name and type, and type specific options. You can also test the notification to make
You specify name and type, and type specific options. You can also test the notification to make
sure it's working and setup correctly.
<!-- You can reach this page from the Alerting submenu or Alert List page header. -->
<!-- When you configure a notification you can have it be a global notifiations, meaning -->
<!-- it will be sent for all alerts within Grafana. This is useful to make sure you won’t miss to configure -->
<!-- notifications for an alert. You can find the alert notification page in the main menu under alerting. -->
<!-- -->
<!-- ## Add a notifications to an Alert -->
<!-- You can add and remove notifications from an alert by going to the `Notifications` sub menu in the alerting tab. -->
<!-- -->
<!-- -->
<!-- <img class="no-shadow" src="/img/docs/v4/alerttab_notifications_submenu.png"> -->
<!-- -->
<!-- -->
<!-- Click the `+` button to add a new notification and the `x` to remove. Notifications with a blue backgrounds are enabled by default for all alerts and cannot be modified from this view. -->
<!-- -->
<!-- -->
<!-- <img class="no-shadow" src="/img/docs/v4/add_remove_notifications.png"> -->
<!-- -->
### Send on all alerts
This option will make this notification used for all alert rules, existing and new.
When checked this option will make this notification used for all alert rules, existing and new.
## Supported notification types
......@@ -61,12 +42,25 @@ To enable email notification you have to setup [SMTP settings](/installation/con
in the Grafana config. Email notification will upload an image of the alert graph to an
external image destination if available or fallback on attaching the image in the email.
### Slack
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
Setting | Description
---------- | -----------
Recipient | allows you to override the slack recipient.
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
### Webhook
The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint.
Using this notification you could integrated Grafana into any system you choose, by yourself.
Example json schema:
Example json body:
```json
{
"title": "My alert",
......@@ -85,19 +79,6 @@ Example json schema:
}
```
### Slack
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
Setting | Description
---------- | -----------
Recipient | allows you to override the slack recipient.
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
### PagerDuty
To set up PagerDuty, all you have to do is to provide an api key.
......@@ -120,7 +101,3 @@ config file.
This is not an optional requirement, you can get slack and email notifications without setting this up.
+++
title = "Alerting Engine Rules Guide"
title = "Alerting Engine & Rules Guide"
description = "Configuring Alert Rules"
keywords = ["grafana", "alerting", "guide", "rules"]
type = "docs"
......@@ -73,7 +73,7 @@ in the scenario below.
- No new notifications are sent as the alert rule is already in state `Alerting`.
So as you can see from the above scenario Grafana will not send out notifications when other series cause the alert
to fire if the rule already is in state ´Alerting`. To improve support for queries that return multiple series
to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series
we plan to track state **per series** in a future release.
### No Data / Null values
......@@ -107,6 +107,12 @@ The message can contain anything, information about how you might solve the issu
The actual notifications are configured and shared between multiple alerts. Read the
[Notifications]({{< relref "notifications.md" >}}) guide for how to configure and setup notifications.
## Alert State History & Annotations
Alert state changes are recorded in the internal annotation table in Grafana's database. The state changes
are visualized as annotations in the alert rule's graph panel. You can also go into the `State history`
submenu in the alert tab to view & clear state history.
## Troubleshooting
{{< imgbox max-width="40%" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}}
......
......@@ -14,13 +14,22 @@ weight = 1
Description | Download
------------ | -------------
Stable .deb for Debian-based Linux | [3.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb)
Stable for Debian-based Linux | [3.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb)
Latest Beta for Debian-based Linux | [4.0.0-beta1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.0-1478693311beta1_amd64.deb)
## Install Stable
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb
```
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.1.1-1470047149_amd64.deb
```
## Install Latest Beta
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.0-1478693311beta1_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.1.1-1470047149_amd64.deb
$ sudo dpkg -i grafana_4.0.0-1478693311beta1_amd64.deb
## APT Repository
......
......@@ -14,7 +14,8 @@ weight = 2
Description | Download
------------ | -------------
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm)
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm)
Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-1478693311beta1.x86_64.rpm)
## Install Stable
......@@ -33,6 +34,21 @@ Or install manually using `rpm`.
$ sudo rpm -i --nodeps grafana-3.1.1-1470047149.x86_64.rpm
## Or Install Latest Beta
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-1478693311beta1.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-4.0.0-1478693311beta1.x86_64.rpm
#### On OpenSuse:
$ sudo rpm -i --nodeps grafana-4.0.0-1478693311beta1.x86_64.rpm
## Install via YUM Repository
Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
......
......@@ -13,7 +13,9 @@ weight = 3
Description | Download
------------ | -------------
Stable Zip package for Windows | [grafana.3.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.1.windows-x64.zip)
Latest stable package for Windows | [grafana.3.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.1.windows-x64.zip)
Latest beta package for Windows | [grafana.4.0.0-beta1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.0-beta1.windows-x64.zip)
## Configure
......
......@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "4.0.0-pre1",
"version": "4.0.0-beta1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
......
......@@ -57,7 +57,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
}
if setting.Env == setting.DEV {
glog.Debug("Influxdb query", "raw query", query)
glog.Debug("Influxdb query", "raw query", rawQuery)
}
req, err := e.createRequest(rawQuery)
......
......@@ -12,7 +12,6 @@ type InfluxdbQueryParser struct{}
func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
policy := model.Get("policy").MustString("default")
rawQuery := model.Get("query").MustString("")
interval := model.Get("interval").MustString("")
alias := model.Get("alias").MustString("")
measurement := model.Get("measurement").MustString("")
......@@ -37,7 +36,8 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo
return nil, err
}
if interval == "" {
interval := model.Get("interval").MustString("")
if interval == "" && dsInfo.JsonData != nil {
dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("")
if dsInterval != "" {
interval = dsInterval
......
......@@ -5,9 +5,15 @@ import (
"strconv"
"strings"
"regexp"
"github.com/grafana/grafana/pkg/tsdb"
)
var (
regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`)
)
type QueryBuilder struct{}
func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) {
......@@ -43,18 +49,28 @@ func (qb *QueryBuilder) renderTags(query *Query) []string {
str += " "
}
value := tag.Value
nValue, err := strconv.ParseFloat(tag.Value, 64)
//If the operator is missing we fall back to sensible defaults
if tag.Operator == "" {
if regexpOperatorPattern.Match([]byte(tag.Value)) {
tag.Operator = "=~"
} else {
tag.Operator = "="
}
}
textValue := ""
numericValue, err := strconv.ParseFloat(tag.Value, 64)
// quote value unless regex or number
if tag.Operator == "=~" || tag.Operator == "!~" {
value = fmt.Sprintf("%s", value)
textValue = tag.Value
} else if err == nil {
value = fmt.Sprintf("%v", nValue)
textValue = fmt.Sprintf("%v", numericValue)
} else {
value = fmt.Sprintf("'%s'", value)
textValue = fmt.Sprintf("'%s'", tag.Value)
}
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, value))
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue))
}
return res
......
......@@ -86,16 +86,34 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(rawQuery, ShouldEqual, `Raw query`)
})
Convey("can render normal tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`)
})
Convey("can render regex tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render regex tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: "value", Key: "key"}}}
query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ value`)
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "1", Key: "key"}}}
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001`)
})
Convey("can render number tags with decimals", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 1`)
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001.1`)
})
Convey("can render string tags", func() {
......
......@@ -22,7 +22,7 @@
<p class="small" style="position: absolute; top: 48px; right: 10px">
<span class="shortcut-table-key">mod</span> =
<span class="muted">CTRL on windows, CMD key on Mac</span>
<span class="muted">CTRL on windows or linux and CMD key on Mac</span>
</p>
<div ng-repeat="(category, shortcuts) in ctrl.shortcuts" class="shortcut-category">
......
......@@ -102,6 +102,7 @@ export default class TimeSeries {
this.stats.min = Number.MAX_VALUE;
this.stats.avg = null;
this.stats.current = null;
this.stats.timeStep = Number.MAX_VALUE;
this.allIsNull = true;
this.allIsZero = true;
......@@ -110,11 +111,22 @@ export default class TimeSeries {
var currentTime;
var currentValue;
var nonNulls = 0;
var previousTime;
for (var i = 0; i < this.datapoints.length; i++) {
currentValue = this.datapoints[i][0];
currentTime = this.datapoints[i][1];
// Due to missing values we could have different timeStep all along the series
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
if (previousTime !== undefined) {
let timeStep = currentTime - previousTime;
if (timeStep < this.stats.timeStep) {
this.stats.timeStep = timeStep;
}
}
previousTime = currentTime;
if (currentValue === null) {
if (ignoreNulls) { continue; }
if (nullAsZero) {
......@@ -145,10 +157,6 @@ export default class TimeSeries {
result.push([currentTime, currentValue]);
}
if (this.datapoints.length >= 2) {
this.stats.timeStep = this.datapoints[1][1] - this.datapoints[0][1];
}
if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; }
if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
......
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
class GrafanaDatasource {
/** @ngInject */
constructor(private backendSrv) {}
query(options) {
return this.backendSrv.get('/api/metrics/test', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
scenario: 'random_walk',
interval: options.intervalMs,
maxDataPoints: options.maxDataPoints
return this.backendSrv.post('/api/tsdb/query', {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: [
{
"refId": "A",
"scenarioId": "random_walk",
"intervalMs": options.intervalMs,
"maxDataPoints": options.maxDataPoints,
}
]
}).then(res => {
var data = [];
if (res.results) {
_.forEach(res.results, queryRes => {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points
});
}
});
}
return {data: data};
});
}
......
......@@ -183,6 +183,24 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
}
}
// Series could have different timeSteps,
// let's find the smallest one so that bars are correctly rendered.
function getMinTimeStepOfSeries(data) {
var min = Number.MAX_VALUE;
for (let i = 0; i < data.length; i++) {
if (!data[i].stats.timeStep) {
continue;
}
if (data[i].stats.timeStep < min) {
min = data[i].stats.timeStep;
}
}
return min;
}
// Function for rendering panel
function render_panel() {
panelWidth = elem.width();
......@@ -279,9 +297,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
break;
}
default: {
if (data.length && data[0].stats.timeStep) {
options.series.bars.barWidth = data[0].stats.timeStep / 1.5;
}
options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
addTimeAxis(options);
break;
}
......
......@@ -2,7 +2,7 @@ define([
'jquery',
'lodash'
],
function ($, _) {
function ($) {
'use strict';
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
......@@ -21,7 +21,10 @@ function ($, _) {
var initial = last*ps;
var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) {
if (series.datapoints.points[j] > posX) {
// Special case of a non stepped line, highlight the very last point just before a null point
if ((series.datapoints.points[initial] != null && series.datapoints.points[j] == null && ! series.lines.steps)
//normal case
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps;
}
}
......@@ -51,23 +54,35 @@ function ($, _) {
//now we know the current X (j) position for X and Y values
var last_value = 0; //needed for stacked values
var minDistance, minTime;
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value & yaxis so that it does not brake series sorting
results.push({ hidden: true, value: 0, yaxis: 0 });
continue;
}
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value & yaxis so that it does not brake series sorting
results.push({ hidden: true, value: 0, yaxis: 0 });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = Math.abs(pos.x - series.data[hoverIndex][0]);
hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (! minDistance
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
......@@ -89,6 +104,7 @@ function ($, _) {
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
}
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0;
if (series.yaxis) {
yaxis = series.yaxis.n;
......@@ -106,8 +122,8 @@ function ($, _) {
});
}
// Find point which closer to pointer
results.time = _.min(results, 'distance').time;
// Time of the point closer to pointer
results.time = minTime;
return results;
};
......@@ -153,6 +169,8 @@ function ($, _) {
seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the
// option is enabled, sort by yaxis by default.
if (panel.tooltip.sort === 2) {
......@@ -169,8 +187,6 @@ function ($, _) {
});
}
var distance, time;
for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i];
......@@ -178,11 +194,6 @@ function ($, _) {
continue;
}
if (! distance || hoverInfo.distance < distance) {
distance = hoverInfo.distance;
time = hoverInfo.time;
}
var highlightClass = '';
if (item && i === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
......@@ -198,7 +209,6 @@ function ($, _) {
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
}
absoluteTime = dashboard.formatDate(time, tooltipFormat);
self.showTooltip(absoluteTime, seriesHtml, pos);
}
// single series tooltip
......
......@@ -66,7 +66,7 @@ class GraphCtrl extends MetricsPanelCtrl {
// fill factor
fill : 1,
// line width in pixels
linewidth : 2,
linewidth : 1,
// show hide points
points : false,
// point radius in pixels
......
......@@ -135,7 +135,7 @@ describe('grafanaGraph', function() {
});
it('should set barWidth', function() {
expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
expect(ctx.plotOptions.series.bars.barWidth).to.be(1/1.5);
});
});
......
......@@ -59,34 +59,34 @@
.dashnav-action-icons,
.dashnav-move-timeframe {
opacity: 0;
transition: opacity 1.5s ease-in-out;
transition: all 1.5s ease-in-out 1s;
}
// navbar buttons
.navbar-brand-btn,
.navbar-inner {
border: none;
border-color: transparent;
background: transparent;
transition: background 1.5s ease-in-out;
transition: all 1.5s ease-in-out 1s;
.fa {
opacity: 0;
transition: opacity 1.5s ease-in-out;
transition: all 1.5s ease-in-out 1s;
}
}
.navbar-page-btn {
border: none;
transform: translate3d(-50px, 0, 0);
border-color: transparent;
background: transparent;
transition: transform 1.5s ease-in-out;
transform: translate3d(-50px, 0, 0);
transition: all 1.5s ease-in-out 1s;
.icon-gf {
opacity: 0;
transition: opacity 1.5s ease-in-out;
transition: all 1.5s ease-in-out 1s;
}
}
.gf-timepicker-nav-btn {
transform: translate3d(40px, 0, 0);
transition: transform 1.5s ease-in-out;
transition: transform 1.5s ease-in-out 1s;
}
}
......@@ -50,7 +50,6 @@
.panel-alert-state {
&--alerting {
background-color: mix($critical,$panel-bg, 3%);
animation: alerting-panel 1.6s cubic-bezier(1,.1,.73,1) 0s infinite alternate;
box-shadow: 0 0 10px rgba($critical,0.5);
opacity: 1;
......
......@@ -1201,24 +1201,21 @@ Licensed under the MIT license.
points[k + m] = null;
}
}
else {
// a little bit of line specific stuff that
// perhaps shouldn't be here, but lacking
// better means...
if (insertSteps && k > 0
&& points[k - ps] != null
&& points[k - ps] != points[k]
&& points[k - ps + 1] != points[k + 1]) {
// copy the point to make room for a middle point
for (m = 0; m < ps; ++m)
points[k + ps + m] = points[k + m];
// middle point has same y
points[k + 1] = points[k - ps + 1];
// we've added a point, better reflect that
k += ps;
}
if (insertSteps && k > 0 && (!nullify || points[k - ps] != null)) {
// copy the point to make room for a middle point
for (m = 0; m < ps; ++m)
points[k + ps + m] = points[k + m];
// middle point has same y
points[k + 1] = points[k - ps + 1] || 0;
// if series has null values, let's give the last !null value a nice step
if(nullify)
points[k] = p[0];
// we've added a point, better reflect that
k += ps;
}
}
}
......
......@@ -78,41 +78,46 @@ charts or filled areas).
i = 0, j = 0, l, m;
while (true) {
if (i >= points.length)
// browse all points from the current series and from the previous series
if (i >= points.length && j >= otherpoints.length)
break;
// newpoints will replace current series with
// as many points as different timestamps we have in the 2 (current & previous) series
l = newpoints.length;
if (points[i] == null) {
// copy gaps
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
if (i < points.length && px == null) {
// let's ignore null points from current series, nothing to do with them
i += ps;
}
else if (j >= otherpoints.length) {
// for lines, we can't use the rest of the points
if (!withlines) {
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
}
i += ps;
else if (j < otherpoints.length && qx == null) {
// let's ignore null points from previous series, nothing to do with them
j += otherps;
}
else if (otherpoints[j] == null) {
// oops, got a gap
else if (i >= points.length) {
// no more points in the current series, simply take the remaining points
// from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m)
newpoints.push(null);
fromgap = true;
newpoints.push(otherpoints[j + m]);
bottom = qy;
j += otherps;
}
else if (j >= otherpoints.length) {
// no more points in the previous series, of course let's take
// the remaining points from the current series
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else {
// cases where we actually got two points
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
// next available points from current and previous series have the same timestamp
if (px == qx) {
// so take the point from the current series and skip the previous' one
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
......@@ -122,55 +127,39 @@ charts or filled areas).
i += ps;
j += otherps;
}
// next available point with the smallest timestamp is from the previous series
else if (px > qx) {
// we got past point below, might need to
// insert interpolated extra point
if (withlines && i > 0 && points[i - ps] != null) {
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
newpoints.push(qx);
newpoints.push(intery + qy);
for (m = 2; m < ps; ++m)
newpoints.push(points[i + m]);
bottom = qy;
}
// so take the point from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
// we might be able to interpolate
if (i > 0 && points[i - ps] != null)
newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
bottom = qy;
j += otherps;
}
else { // px < qx
if (fromgap && withlines) {
// if we come from a gap, we just skip this point
i += ps;
continue;
}
// (px < qx) next available point with the smallest timestamp is from the current series
else {
// so of course let's take the point from the current series
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
// we might be able to interpolate a point below,
// this can give us a better y
if (withlines && j > 0 && otherpoints[j - otherps] != null)
if (j > 0 && otherpoints[j - otherps] != null)
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
newpoints[l + accumulateOffset] += bottom;
i += ps;
}
}
fromgap = false;
if (l != newpoints.length && withbottom)
newpoints[l + 2] += bottom;
}
// maintain the line steps invariant
if (withsteps && l != newpoints.length && l > 0
&& newpoints[l] != null
&& newpoints[l] != newpoints[l - ps]
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
for (m = 0; m < ps; ++m)
newpoints[l + ps + m] = newpoints[l + m];
newpoints[l + 1] = newpoints[l - ps + 1];
}
if (l != newpoints.length && withbottom)
newpoints[l + 2] = bottom;
}
datapoints.points = newpoints;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment