blob: 47cb47d3833b9c20d0dfded246380a3c87dbf1f8 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
/**
* @module QDR
*/
var QDR = (function(QDR) {
// The QDR chart service handles periodic gathering data for charts and displaying the charts
QDR.module.factory("QDRChartService", ['$rootScope', 'QDRService', '$http', '$resource', '$location',
function($rootScope, QDRService, $http, $resource, $location) {
var instance = 0; // counter for chart instances
var bases = [];
var findBase = function (name, attr, request) {
for (var i=0; i<bases.length; ++i) {
var base = bases[i];
if (base.equals(name, attr, request))
return base;
}
return null;
}
function ChartBase(name, attr, request) {
// the base chart attributes
this.name = name; // the record's "name" field
this.attr = attr; // the record's attr field to chart
this.request = request; // the associated request that fetches the data
// copy the savable properties to an object
this.copyProps = function (o) {
o.name = this.name;
o.attr = this.attr;
this.request.copyProps(o);
}
this.equals = function (name, attr, request) {
return (this.name == name && this.attr == attr && this.request.equals(request));
}
};
// Object that represents a visible chart
// There can be multiple of these per ChartBase (eg. one rate and one value chart)
function Chart(opts, request) { //name, attr, cinstance, request) {
var base = findBase(opts.name, opts.attr, request);
if (!base) {
base = new ChartBase(opts.name, opts.attr, request);
bases.push(base);
}
this.base = base;
this.instance = angular.isDefined(opts.instance) ? opts.instance : ++instance;
this.dashboard = false; // is this chart on the dashboard page
this.hdash = false; // is this chart on the hawtio dashboard page
this.hreq = false; // has this hdash chart been requested
this.type = opts.type ? opts.type: "value"; // value or rate
this.rateWindow = opts.rateWindow ? opts.rateWindow : 1000; // calculate the rate of change over this time interval. higher == smother graph
this.areaColor = "#cbe7f3"; // the chart's area color when not an empty string
this.lineColor = "#058dc7"; // the chart's line color when not an empty string
this.visibleDuration = opts.visibleDuration ? opts.visibleDuration : 10; // number of minutes of data to show (<= base.duration)
this.userTitle = null; // user title overrides title()
// generate a unique id for this chart
this.id = function () {
var name = this.name()
var nameparts = name.split('/');
if (nameparts.length == 2)
name = nameparts[1];
var key = QDRService.nameFromId(this.request().nodeId) + this.request().entity + name + this.attr() + "_" + this.instance + "_" + (this.request().aggregate ? "1" : "0");
// remove all characters except letters,numbers, and _
return key.replace(/[^\w]/gi, '')
}
// copy the savable properties to an object
this.copyProps = function (o) {
o.type = this.type;
o.rateWindow = this.rateWindow;
o.areaColor = this.areaColor;
o.lineColor = this.lineColor;
o.visibleDuration = this.visibleDuration;
o.userTitle = this.userTitle;
o.dashboard = this.dashboard;
o.hdash = this.hdash;
o.instance = this.instance;
this.base.copyProps(o);
}
this.name = function (_) {
if (!arguments.length) return this.base.name;
this.base.name = _;
return this;
}
this.attr = function (_) {
if (!arguments.length) return this.base.attr;
this.base.attr = _;
return this;
}
this.nodeId = function (_) {
if (!arguments.length) return this.base.request.nodeId;
this.base.request.nodeId = _;
return this;
}
this.entity = function (_) {
if (!arguments.length) return this.base.request.entity;
this.base.request.entity = _;
return this;
}
this.aggregate = function (_) {
if (!arguments.length) return this.base.request.aggregate;
this.base.request.aggregate = _;
return this;
}
this.request = function (_) {
if (!arguments.length) return this.base.request;
this.base.request = _;
return this;
}
this.data = function () {
return this.base.request.data(this.base.name, this.base.attr); // refernce to chart's data array
}
this.interval = function (_) {
if (!arguments.length) return this.base.request.interval;
this.base.request.interval = _;
return this;
}
this.duration = function (_) {
if (!arguments.length) return this.base.request.duration;
this.base.request.duration = _;
return this;
}
this.title = function (_) {
var name = this.request().aggregate ? 'Aggregate' : QDRService.nameFromId(this.nodeId());
var computed = name +
" " + QDRService.humanify(this.attr()) +
" - " + this.name()
if (!arguments.length) return this.userTitle || computed;
// don't store computed title in userTitle
if (_ === computed)
_ = null;
this.userTitle = _;
return this;
}
this.title_short = function (_) {
if (!arguments.length) return this.userTitle || this.name();
return this;
}
this.copy = function () {
var chart = self.registerChart({
nodeId: this.nodeId(),
entity: this.entity(),
name: this.name(),
attr: this.attr(),
interval: this.interval(),
forceCreate: true,
aggregate: this.aggregate(),
hdash: this.hdash
})
chart.type = this.type;
chart.areaColor = this.areaColor;
chart.lineColor = this.lineColor;
chart.rateWindow = this.rateWindow;
chart.visibleDuration = this.visibleDuration;
chart.userTitle = this.userTitle;
return chart;
}
// compare to a chart
this.equals = function (c) {
return (c.instance == this.instance &&
c.base.equals(this.base.name, this.base.attr, this.base.request) &&
c.type == this.type &&
c.rateWindow == this.rateWindow &&
c.areaColor == this.areaColor &&
c.lineColor == this.lineColor)
}
}
// Object that represents the management request to fetch and store data for multiple charts
function ChartRequest(opts) { //nodeId, entity, name, attr, interval, aggregate) {
this.duration = opts.duration || 10; // number of minutes to keep the data
this.nodeId = opts.nodeId; // eg amqp:/_topo/0/QDR.A/$management
this.entity = opts.entity; // eg .router.address
// sorted since the responses will always be sorted
this.aggregate = opts.aggregate; // list of nodeIds for aggregate charts
this.datum = {}; // object containing array of arrays for each attr
// like {attr1: [[date,value],[date,value]...], attr2: [[date,value]...]}
this.interval = opts.interval || 1000; // number of milliseconds between updates to data
this.setTimeoutHandle = null; // used to cancel the next request
// copy the savable properties to an object
this.data = function (name, attr) {
if (this.datum[name] && this.datum[name][attr])
return this.datum[name][attr]
return null;
}
this.addAttrName = function (name, attr) {
if (Object.keys(this.datum).indexOf(name) == -1) {
this.datum[name] = {}
}
if (Object.keys(this.datum[name]).indexOf(attr) == -1) {
this.datum[name][attr] = [];
}
}
this.addAttrName(opts.name, opts.attr)
this.copyProps = function (o) {
o.nodeId = this.nodeId;
o.entity = this.entity;
o.interval = this.interval;
o.aggregate = this.aggregate;
o.duration = this.duration;
}
this.removeAttr = function (name, attr) {
if (this.datum[name]) {
if (this.datum[name][attr]) {
delete this.datum[name][attr]
}
}
return this.attrs().length;
}
this.equals = function (r, entity, aggregate) {
if (arguments.length == 3) {
var o = {nodeId: r, entity: entity, aggregate: aggregate}
r = o;
}
return (this.nodeId === r.nodeId && this.entity === r.entity && this.aggregate == r.aggregate)
}
this.names = function () {
return Object.keys(this.datum)
}
this.attrs = function () {
var attrs = {}
Object.keys(this.datum).forEach( function (name) {
Object.keys(this.datum[name]).forEach( function (attr) {
attrs[attr] = 1;
})
}, this)
return Object.keys(attrs);
}
};
// Below here are the properties and methods available on QDRChartService
var self = {
charts: [], // list of charts to gather data for
chartRequests: [], // the management request info (multiple charts can be driven off of a single request
init: function () {
self.loadCharts();
},
findChartRequest: function (nodeId, entity, aggregate) {
var ret = null;
self.chartRequests.some( function (request) {
if (request.equals(nodeId, entity, aggregate)) {
ret = request;
return true;
}
})
return ret;
},
findCharts: function (opts) { //name, attr, nodeId, entity, hdash) {
if (!opts.hdash)
opts.hdash = false; // rather than undefined
return self.charts.filter( function (chart) {
return (chart.name() == opts.name &&
chart.attr() == opts.attr &&
chart.nodeId() == opts.nodeId &&
chart.entity() == opts.entity &&
chart.hdash == opts.hdash)
});
},
delChartRequest: function (request) {
for (var i=0; i<self.chartRequests.length; ++i) {
var r = self.chartRequests[i];
if (request.equals(r)) {
QDR.log.debug("removed request: " + request.nodeId + " " + request.entity);
self.chartRequests.splice(i, 1);
self.stopCollecting(request);
return;
}
}
},
delChart: function (chart) {
var foundBases = 0;
for (var i=0; i<self.charts.length; ++i) {
var c = self.charts[i];
if (c.base === chart.base)
++foundBases;
if (c.equals(chart)) {
self.charts.splice(i, 1);
if (chart.dashboard)
self.saveCharts();
}
}
if (foundBases == 1) {
var baseIndex = bases.indexOf(chart.base)
bases.splice(baseIndex, 1);
}
},
registerChart: function (opts) { //nodeId, entity, name, attr, interval, instance, forceCreate, aggregate, hdash) {
var request = self.findChartRequest(opts.nodeId, opts.entity, opts.aggregate);
if (request) {
// add any new attr or name to the list
request.addAttrName(opts.name, opts.attr)
} else {
// the nodeId/entity did not already exist, so add a new request and chart
QDR.log.debug("added new request: " + opts.nodeId + " " + opts.entity);
request = new ChartRequest(opts); //nodeId, entity, name, attr, interval, aggregate);
self.chartRequests.push(request);
self.startCollecting(request);
}
var charts = self.findCharts(opts); //name, attr, nodeId, entity, hdash);
var chart;
if (charts.length == 0 || opts.forceCreate) {
if (!opts.use_instance && opts.instance)
delete opts.instance;
chart = new Chart(opts, request) //opts.name, opts.attr, opts.instance, request);
self.charts.push(chart);
} else {
chart = charts[0];
}
return chart;
},
// remove the chart for name/attr
// if all attrs are gone for this request, remove the request
unRegisterChart: function (chart) {
// remove the chart
// TODO: how do we remove charts that were added to the hawtio dashboard but then removed?
// We don't get a notification that they were removed. Instead, we could just stop sending
// the request in the background and only send the request when the chart's tick() event is triggered
//if (chart.hdash) {
// chart.dashboard = false;
// self.saveCharts();
// return;
//}
for (var i=0; i<self.charts.length; ++i) {
var c = self.charts[i];
if (chart.equals(c)) {
var request = chart.request();
self.delChart(chart);
if (request) {
// see if any other charts use this attr
for (var i=0; i<self.charts.length; ++i) {
var c = self.charts[i];
if (c.attr() == chart.attr() && c.request().equals(chart.request()))
return;
}
// no other charts use this attr, so remove it
if (request.removeAttr(chart.name(), chart.attr()) == 0) {
self.stopCollecting(request);
self.delChartRequest(request);
}
}
}
}
self.saveCharts();
},
stopCollecting: function (request) {
if (request.setTimeoutHandle) {
clearTimeout(request.setTimeoutHandle);
request.setTimeoutHandle = null;
}
},
startCollecting: function (request) {
// Using setTimeout instead of setInterval because the response may take longer than interval
request.setTimeoutHandle = setTimeout(self.sendChartRequest, request.interval, request);
},
shouldRequest: function (request) {
// see if any of the charts associated with this request have either dialog, dashboard, or hreq
return self.charts.some( function (chart) {
return (chart.dashboard || chart.hreq) || (!chart.dashboard && !chart.hdash);
});
},
// send the request
sendChartRequest: function (request, once) {
if (!once && !self.shouldRequest(request)) {
request.setTimeoutHandle = setTimeout(self.sendChartRequest, request.interval, request)
return;
}
// ensure the response has the name field so we can associate the response values with the correct chart
var attrs = request.attrs();
attrs.push("name");
// this is called when the response is received
var saveResponse = function (nodeId, entity, response) {
if (!response || !response.attributeNames)
return;
//QDR.log.debug("got chart results for " + nodeId + " " + entity);
// records is an array that has data for all names
var records = response.results;
if (!records)
return;
var now = new Date();
var cutOff = new Date(now.getTime() - request.duration * 60 * 1000);
// index of the "name" attr in the response
var nameIndex = response.attributeNames.indexOf("name");
if (nameIndex < 0)
return;
var names = request.names();
// for each record returned, find the name/attr for this request and save the data with this timestamp
for (var i=0; i<records.length; ++i) {
var name = records[i][nameIndex];
// if we want to store the values for some attrs for this name
if (names.indexOf(name) > -1) {
attrs.forEach( function (attr) {
var data = request.data(name, attr) // get a reference to the data array
if (data) {
var attrIndex = response.attributeNames.indexOf(attr)
if (request.aggregate) {
data.push([now, response.aggregates[i][attrIndex].sum, response.aggregates[i][attrIndex].detail])
} else {
data.push([now, records[i][attrIndex]])
}
// expire the old data
while (data[0][0] < cutOff) {
data.shift();
}
}
})
}
}
}
if (request.aggregate) {
var nodeList = QDRService.nodeIdList()
QDRService.getMultipleNodeInfo(nodeList, request.entity, attrs, saveResponse, request.nodeId);
} else {
QDRService.getNodeInfo(request.nodeId, request.entity, attrs, saveResponse);
}
// it is now safe to schedule another request
if (once)
return;
request.setTimeoutHandle = setTimeout(self.sendChartRequest, request.interval, request)
},
numCharts: function () {
return self.charts.filter( function (chart) { return chart.dashboard }).length;
//return self.charts.length;
},
isAttrCharted: function (nodeId, entity, name, attr) {
var charts = self.findCharts({
name: name,
attr: attr,
nodeId: nodeId,
entity: entity
})
// if any of the matching charts are on the dashboard page, return true
return charts.some(function (chart) {
return (chart.dashboard) });
},
addHDash: function (chart) {
chart.hdash = true;
self.saveCharts();
/*
if (!chart.hdash) {
var dashChart = self.registerChart(chart.nodeId(), chart.entity(),
chart.name(), chart.attr(), chart.interval(), true, chart.aggregate(), true);
dashChart.dashboard = true;
dashChart.hdash = false;
chart.dashboard = false;
chart.hdash = true;
self.saveCharts();
}
*/
},
delHDash: function (chart) {
chart.hdash = false;
self.saveCharts();
},
addDashboard: function (chart) {
chart.dashboard = true;
self.saveCharts();
},
delDashboard: function (chart) {
chart.dashboard = false;
self.saveCharts();
},
// save the charts to local storage
saveCharts: function () {
var charts = [];
var minCharts = [];
self.charts.forEach(function (chart) {
var minChart = {};
// don't save chart unless it is on the dashboard
if (chart.dashboard || chart.hdash) {
chart.copyProps(minChart);
minCharts.push(minChart);
}
})
localStorage["QDRCharts"] = angular.toJson(minCharts);
},
loadCharts: function () {
var charts = angular.fromJson(localStorage["QDRCharts"]);
if (charts) {
var nodeList = QDRService.nodeList().map( function (node) {
return node.id;
})
charts.forEach(function (chart) {
if (nodeList.indexOf(chart.nodeId) >= 0) {
if (!chart.interval)
chart.interval = 1000;
if (!chart.duration)
chart.duration = 10;
if (chart.nodeList)
chart.aggregate = true;
var newChart = self.registerChart(chart.nodeId, chart.entity, chart.name, chart.attr, chart.interval, true, chart.aggregate);
newChart.dashboard = true; // we only save the dashboard charts
newChart.type = chart.type;
newChart.rateWindow = chart.rateWindow;
newChart.areaColor = chart.areaColor ? chart.areaColor : "#c0e0ff";
newChart.lineColor = chart.lineColor ? chart.lineColor : "#4682b4";
newChart.duration(chart.duration);
newChart.visibleDuration = chart.visibleDuration ? chart.visibleDuration : 10;
if (chart.userTitle)
newChart.title(chart.userTitle);
}
})
}
},
loadCharts: function () {
var charts = angular.fromJson(localStorage["QDRCharts"]);
if (charts) {
// get array of known ids
var nodeList = QDRService.nodeList().map( function (node) {
return node.id;
})
charts.forEach(function (chart) {
// if this chart is not in the current list of nodes, skip
if (nodeList.indexOf(chart.nodeId) >= 0) {
if (!angular.isDefined(chart.instance)) {
chart.instance = ++instance;
}
if (chart.instance >= instance)
instance = chart.instance + 1;
if (!chart.duration)
chart.duration = 10;
if (chart.nodeList)
chart.aggregate = true;
if (!chart.hdash)
chart.hdash = false;
if (!chart.dashboard)
chart.dashboard = false;
if (!chart.hdash && !chart.dashboard)
chart.dashboard = true;
if (chart.hdash && chart.dashboard)
chart.dashboard = false;
chart.forceCreate = true;
chart.use_instance = true;
var newChart = self.registerChart(chart); //chart.nodeId, chart.entity, chart.name, chart.attr, chart.interval, true, chart.aggregate);
newChart.dashboard = chart.dashboard;
newChart.hdash = chart.hdash;
newChart.hreq = false;
newChart.type = chart.type;
newChart.rateWindow = chart.rateWindow;
newChart.areaColor = chart.areaColor ? chart.areaColor : "#cbe7f3";
newChart.lineColor = chart.lineColor ? chart.lineColor : "#058dc7";
newChart.duration(chart.duration);
newChart.visibleDuration = chart.visibleDuration ? chart.visibleDuration : 10;
if (chart.userTitle)
newChart.title(chart.userTitle);
}
})
}
},
AreaChart: function (chart) {
if (!chart)
return;
// if this is an aggregate chart, show it stacked
var stacked = chart.request().aggregate;
this.chart = chart; // reference to underlying chart
this.svgchart = null;
this.url = $location.absUrl();
// callback function. called by svgchart when binding data
// the variable 'this' refers to the svg and not the AreaChart,
// but since we are still in the scope of the AreaChart we have access to the passed in chart argument
this.chartData = function () {
var now = new Date();
var visibleDate = new Date(now.getTime() - chart.visibleDuration * 60 * 1000);
var data = chart.data();
var nodeList = QDRService.nodeIdList();
if (chart.type == "rate") {
var rateData = [];
var datalen = data.length;
k = 0; // inner loop optimization
for (var i=0; i<datalen; ++i) {
var d = data[i];
if (d[0] >= visibleDate) {
for (var j=k+1; j<datalen; ++j) {
var d1 = data[j];
if (d1[0] - d[0] >= chart.rateWindow) { // rateWindow is the timespan to calculate rates
var elapsed = Math.max((d1[0] - d[0]) / 1000, 1); // number of seconds that elapsed
var rd = [d1[0],(d1[1] - d[1])/elapsed]
k = j; // start here next time
// this is a stacked (aggregate) chart
if (stacked) {
var detail = [];
nodeList.forEach( function (node, nodeIndex) {
if (d1[2][nodeIndex] && d[2][nodeIndex])
detail.push({node: QDRService.nameFromId(node), val: (d1[2][nodeIndex].val- d[2][nodeIndex].val)/elapsed})
})
rd.push(detail)
}
rateData.push(rd);
break;
}
}
}
}
// we need at least a point to chart
if (rateData.length == 0) {
rateData[0] = [chart.data()[0][0],0,[{node:'',val:0}]];
}
return rateData;
}
if (chart.visibleDuration != chart.duration()) {
return data.filter(function (d) { return d[0]>=visibleDate});
} else
return data;
}
this.zoom = function (id, zoom) {
if (this.svgchart) {
this.svgchart.attr("zoom", zoom)
d3.select('#' + id)
.data([this.chartData()])
.call(this.svgchart)
}
}
// called by the controller on the page that displays the chart
// called whenever the controller wants to redraw the chart
// note: the data is collected independently of how often the chart is redrawn
this.tick = function (id) {
// can't draw charts that don't have data yet
if (this.chart.data().length == 0) {
return;
}
// if we haven't created the svg yet
if (!this.svgchart) {
// make sure the dom element exists on the page
var div = angular.element('#' + id);
if (!div)
return;
var width = div.width();
var height = div.height();
// make sure the dom element has a size. otherwise we wouldn't see anything anyway
if (!width)
return;
var tooltipGenerator;
// stacked charts have a different tooltip
if (stacked) {
tooltipGenerator = function (d, color, format) {
var html = "<table class='fo-table'><tbody><tr class='fo-title'>"+
"<td align='center' colspan='2' nowrap>Time: "+d[0].toTimeString().substring(0, 8)+"</td></tr>"
d[2].forEach( function (detail) {
html += "<tr class='detail'><td align='right' nowrap>"
+ detail.node
+ "<div class='fo-table-legend' style='background-color: "+color(detail.node)+"'></div>"
+ "</td><td>"+format(detail.val)+"</td></tr>"
})
html += "</tbody></table>"
return html;
}
} else {
tooltipGenerator = function (d, color, format) {
var html = "<table class='fo-table'><tbody><tr class='fo-title'>"+
"<td align='center'>Time</td><td align='center'>Value</td></tr><tr><td>" +
d[0].toTimeString().substring(0, 8) +
"</td><td>" +
format(d[1]) +
"</td></tr></tbody></table>"
return html;
}
}
// create and initialize the chart
this.svgchart = self.timeSeriesStackedChart(id, width, height,
QDRService.humanify(this.chart.attr()),
this.chart.name(),
QDRService.nameFromId(this.chart.nodeId()),
this.chart.entity(),
stacked,
this.chart.visibleDuration)
.tooltipGenerator(tooltipGenerator);
}
// in case the chart properties have changed, set the new props
this.svgchart
.attr("type", this.chart.type)
.attr("areaColor", this.chart.areaColor)
.attr("lineColor", this.chart.lineColor)
.attr("url", this.url)
.attr("title", this.chart.userTitle);
// bind the new data and update the chart
d3.select('#' + id) // the div id on the page/dialog
.data([this.chartData()])
.call(this.svgchart); // the charting function
}
},
timeSeriesStackedChart: function (id, width, height, attrName, name, node, entity, stacked, visibleDuration) {
var margin = {top: 20, right: 18, bottom: 10, left: 15}
// attrs that can be changed after the chart is created by using
// chart.attr(<attrname>, <attrvalue>);
var attrs = {
attrName: attrName, // like Deliveries to Container. Put at top of chart
name: name, // like router.address/qdrhello Put at bottom of chart with node
node: node, // put at bottom of chart with name
entity: entity, // like .router.address Not used atm
title: "", // user title overrides the node and name at the bottom of the chart
url: "", // needed to reference filters and clip because of angular's location service
type: "value", // value or rate
areaColor: "", // can be set for non-stacked charts
lineColor: "", // can be set for non-stacked charts
zoom: false, // should the y-axis range start at 0 or the min data value
visibleDuration: visibleDuration
}
var width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom,
yAxisTransitionDuration = 0
var x = d3.time.scale()
var y = d3.scale.linear()
.rangeRound([height, 0]);
// The x-accessor for the path generator; xScale * xValue.
var X = function (d) { return x(d[0]) }
// The x-accessor for the path generator; yScale * yValue.
var Y = function Y(d) { return y(d[1]) }
var xAxis = d3.svg.axis().scale(x).orient("bottom")
.outerTickSize(6)
.innerTickSize(-(height-margin.top-margin.bottom))
.tickPadding(2)
.ticks(d3.time.minutes, 2)
var yAxis = d3.svg.axis().scale(y).orient("right")
.outerTickSize(8)
.innerTickSize(-(width-margin.left-margin.right))
.tickPadding(10)
.ticks(3)
.tickFormat(function(d) { return formatValue(d)})
var tooltipGenerator = function (d, color, format) {return ""}; // should be overridden to set an appropriate tooltip
var formatValue = d3.format(".2s");
var formatPrecise = d3.format(",");
var bisectDate = d3.bisector(function(d) { return d[0]; }).left;
var line = d3.svg.line();
var stack = d3.layout.stack()
.offset("zero")
.values(function (d) { return d.values; })
.x(function (d) { return x(d.date); })
.y(function (d) { return d.value; });
var area = d3.svg.area()
if (stacked) {
area.interpolate("cardinal")
.x(function (d) { return x(d.date); })
.y0(function (d) { return y(d.y0); })
.y1(function (d) { return y(d.y0 + d.y); });
} else {
area.interpolate("basis").x(X).y1(Y)
line.x(X).y(Y)
}
var color = d3.scale.category20();
var sv = d3.select("#"+id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svg = sv
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
sv.append("linearGradient")
.attr("id", id) //"temperature-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", height *.5 )
.attr("x2", 0).attr("y2", height * 1.2)
.selectAll("stop")
.data([
{offset: "0%", opacity: 1},
{offset: "100%", opacity: 0}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-opacity", function(d) { return d.opacity; })
.attr("stop-color", function(d) { return "#cbe7f3" });
/*
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr("width", width)
.attr("height", height);
*/
// we want all our areas to appear before the axiis
svg.append("g")
.attr("class", "section-container")
svg.append("g")
.attr("class", "x axis")
svg.append("g")
.attr("class", "y axis")
svg.append("text").attr("class", "title")
.attr("x", (width / 2) - (margin.left + margin.right) / 2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text(attrs.attrName);
svg.append("text").attr("class", "legend")
.attr("x", (width / 2) - (margin.left + margin.right) / 2)
.attr("y", height + (margin.bottom / 2) )
.attr("text-anchor", "middle")
.text(!stacked ? attrs.node + " " + attrs.name : attrs.name);
var focus = sv.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
var focusg = focus.append("g");
focusg.append("rect")
.attr("class", "mo-guide y")
.attr("width", 1)
.attr("height", height - (margin.top + margin.bottom));
focusg.append("rect")
.attr("class", "mo-guide x")
.attr("width", width - (margin.left + margin.right))
.attr("height", 1);
focus.append("foreignObject")
.attr('class', 'svg-tooltip')
.append("xhtml:span");
/*
var transition = d3.select({}).transition()
.duration(2000)
.ease("linear");
*/
function chart(selection) {
selection.each(function(data) {
var seriesArr = []
if (stacked) {
var detailNames = data[0][2].map(function (detail){ return detail.node })
var revNames = angular.copy(detailNames).reverse();
color.domain(revNames);
var series = {};
detailNames.forEach(function (name) {
series[name] = {name: name, values:[]};
seriesArr.unshift(series[name]); // insert at beginning
});
data.forEach(function (d) {
detailNames.map(function (name, i) {
series[name].values.push({date: d[0], value: d[2][i] ? d[2][i].val : 0});
});
});
// this decorates seriesArr with x,y,and y0 properties
stack(seriesArr);
}
var extent = d3.extent(data, function(d) {return d[0];});
//var points = data.length;
//var futureDate = new Date(data[points-1][0].getTime() - attrs.visibleDuration * 60 * 1000);
//extent = [futureDate, data[points-1][0]]
x.domain(extent)
.range([0, width - margin.left - margin.right]);
// Update the y-scale.
var min = attrs.zoom ? 0 : d3.min(data, function(d) {return d[1]}) *.99;
var max = d3.max(data, function(d) {return d[1]}) * 1.01;
var mean = d3.mean(data, function(d) {return d[1]});
//max = max * 1.01;
var diff = (max - min);
if (diff == 0) {
max = max + 1;
diff = 1;
}
var ratio = mean != 0 ? diff / mean : 1;
if (ratio < .05)
formatValue = d3.format(".3s")
if (stacked) {
y.domain([min, max])
.range([height - margin.top - margin.bottom, 0]);
} else {
y
.domain([min, max])
.range([height - margin.top - margin.bottom, 0]);
}
if (attrs.type == "rate") {
area.interpolate("basis"); // rate charts look better smoothed
line.interpolate("basis");
}
else {
area.interpolate("linear"); // don't smooth value charts
line.interpolate("linear");
}
// adjust the xaxis based on the range of x values (domain)
var timeSpan = (extent[1] - extent[0]) / (1000 * 60); // number of minutes
if (timeSpan < 1.5)
xAxis.ticks(d3.time.seconds, 10);
else if (timeSpan < 3)
xAxis.ticks(d3.time.seconds, 30);
else if (timeSpan < 8)
xAxis.ticks(d3.time.minutes, 1);
else
xAxis.ticks(d3.time.minutes, 2);
// adjust the number of yaxis ticks based on the range of y values
if (formatValue(min) === formatValue(max))
yAxis.ticks(2);
var container = svg.select('.section-container');
container.selectAll('.series').remove();
if (stacked) {
y.domain([Math.min(min, 0), d3.max(seriesArr, function (c) {
return d3.max(c.values, function (d) { return d.y0 + d.y; });
})]);
// creates a .series g path for each section in the detail
// since we don't get more sections this selection is only run once
var series = container.selectAll(".series")
.data(seriesArr)
series.enter().append("g")
.attr("class", "series")
.append("path")
.attr("class", "streamPath")
.style("fill", function (d) { return color(d.name); })
.style("stroke", "grey");
series.exit().remove()
// each time the data is updated, update each section
container.selectAll(".series .streamPath").data(seriesArr)
.attr("d", function (d) { return area(d.values); })
} else {
var series = container.selectAll(".series")
.data([data], function(d) { return d; })
var g = series.enter().append("g")
.attr("class", "series")
g.append("path")
.attr("class", "area")
.style("fill", "url(" + attrs.url + "#" + id + ") " + attrs.areaColor) //temperature-gradient)")
.attr("d", area.y0(y.range()[0]))
.attr("transform", null);
g.append("path")
.attr("class", "line")
.style("stroke", attrs.lineColor)
.attr("d", line)
/*
debugger;
g.transition()
.duration(2000)
.attr("transform", "translate(-4)");
*/
series.exit().remove()
sv.selectAll("stop")
.attr("stop-color", attrs.areaColor)
}
// Update the x-axis.
svg.select(".x.axis")
.attr("transform", "translate(0," + (height - margin.top - margin.bottom + 1) + ")")
.call(xAxis);
svg.select(".y.axis")
.transition().duration(yAxisTransitionDuration) // animate the y axis
.attr("transform", "translate(" + (width - margin.right - margin.left) + ",0)")
.call(yAxis);
yAxisTransitionDuration = 1000 // only do a transition after the chart is 1st drawn
// TODO: fix this
// need to recreate this every update... not sure why
var overlay = sv.select(".overlay");
if (!overlay.empty())
overlay.remove();
sv.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function () {focus.style("display", null)})
.on("mouseout", function () {focus.style("display", "none")})
.on("mousemove", mousemove)
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0] - margin.left);
var i = bisectDate(data, x0, 1);
if (i < data.length && i > 0) {
var d0 = data[i - 1];
var d1 = data[i];
// set d to the data that is closest to the mouse position
var d = x0 - d0[0] > d1[0] - x0 ? d1 : d0;
focus.attr("transform", "translate(" + (x(d[0]) + margin.left) + "," + (y(d[1]) + margin.top) + ")");
var tipFormat = formatPrecise;
if (attrs.type === "rate")
tipFormat = d3.format(".2n")
// set the tooltip html and position it
focus.select('.svg-tooltip span')
.html(tooltipGenerator(d, color, tipFormat))
var foBounds = focus.select('table')[0][0].getBoundingClientRect();
var mx = x(d[0]); // mouse x
var my = y(d[1]); // mouse y
// perfer to put the tooltip in the nw corner relative to the focus circle
var foy = -foBounds.height;
var fox = -foBounds.width;
// off the left side
if (mx - foBounds.width - margin.left < 0)
fox = 0;
// above the top
if (my - foBounds.height - margin.top < 0)
foy = 0;
// won't fit above or below, just put it at bottom
if (my + foBounds.height > height)
foy = -(foBounds.height - (height - my));
focus.select('.svg-tooltip')
.attr('x', fox).attr('y', foy);
// position the guide lines
focus.select(".mo-guide.y")
.attr("y", -my);
focus.select(".mo-guide.x")
.attr("x", -mx);
} else {
focus.attr("transform", "translate(-10,-10)");
}
}
})
}
chart.attr = function (attrName, value) {
if (arguments.length < 2)
return arguments.length == 1 ? attrs[attrName] : chart;
if (angular.isDefined(attrs[attrName]))
attrs[attrName] = value;
return chart;
}
chart.tooltipGenerator = function (_) {
tooltipGenerator = _;
return chart;
}
return chart;
}
}
return self;
}]);
return QDR;
}(QDR || {}));