425 lines
14 KiB
JavaScript
425 lines
14 KiB
JavaScript
/*
|
|
Draw line chart in d3.
|
|
|
|
To use, a div is required with the data attributes
|
|
data-chart-type="line_chart", data-url.
|
|
|
|
data-chart-type - REQUIRED(string) must be "line_chart" so chart gets initialized
|
|
data-url - REQUIRED(string) url for the json data for the chart
|
|
data-form-selector - Optional(string) JQuery selector of Forms that controls this chart
|
|
data-legend-selector - Optional(string) JQuery selector of div element that will display legend
|
|
data-smoother-selector - Optional(string) JQuery selector of TODO(lsmola)
|
|
data-slider-selector - Optional(string) JQuery selector of TODO(lsmola)
|
|
|
|
|
|
If used in popup, initialization must be made manually e.g.:
|
|
|
|
if (typeof horizon.d3_line_chart !== 'undefined') {
|
|
horizon.d3_line_chart.init("div[data-chart-type='line_chart']");
|
|
}
|
|
|
|
|
|
Example:
|
|
<div id="line_chart"
|
|
data-chart-type="line_chart"
|
|
data-url="{% url 'horizon:admin:metering:samples'%}"
|
|
data-form-selector='#linechart_general_form'>
|
|
</div>
|
|
<div id="linea_chart2"
|
|
data-chart-type="line_chart"
|
|
data-url="{% url 'horizon:admin:metering:samples'%}?query=not_popular_data"
|
|
data-form-selector='#linechart_general_form'>
|
|
</div>
|
|
|
|
|
|
The data format example:
|
|
Url has to return JSON in format:
|
|
{
|
|
"series": [{"name": "instance-00000005",
|
|
"data": [{"y": 171, "x": "2013-08-21T11:22:25"}, {"y": 171, "x": "2013-08-21T11:22:25"}]},
|
|
{"name": "instance-00000005",
|
|
"data": [{"y": 171, "x": "2013-08-21T11:22:25"}, {"y": 171, "x": "2013-08-21T11:22:25"}]}
|
|
],
|
|
"settings": {}
|
|
}
|
|
|
|
|
|
The control Forms:
|
|
There are currently 2 form elements that can be connected to charts and act
|
|
as controls. Elements listen for change event and refresh the chart on change event.
|
|
Chart can be connected to multiple forms via selector, all for data will be sent on
|
|
change.
|
|
Form can be connected to multiple charts, all charts will be refreshed when form
|
|
element changes.
|
|
The firsts rendering of the chart takes data from the connected Forms.
|
|
|
|
1. Selectbox
|
|
The data attribute 'data-line-chart-command="select_box_change' needs to be defined on
|
|
select element.
|
|
|
|
Example:
|
|
<form class="form-horizontal"
|
|
id="linechart_general_form">
|
|
|
|
<div class="control-group">
|
|
<label for="meter" class="control-label">{% trans "Metric" %}: </label>
|
|
<div class="controls">
|
|
<select data-line-chart-command="select_box_change"
|
|
name="meter" id="meter" class="span2 example">
|
|
{% for meter in meters %}
|
|
<option value="{{ meter }}" data-unit="{{ meter }}">
|
|
{{ meter }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
2. Date picker
|
|
The data attribute 'data-line-chart-command="date_picker_change"' needs to be defined on
|
|
input element.
|
|
|
|
Example:
|
|
<form class="form-horizontal"
|
|
id="linechart_general_form">
|
|
<div class="control-group" id="date_from">
|
|
<label for="date_from" class="control-label">{% trans "From" %}: </label>
|
|
<div class="controls">
|
|
<input data-line-chart-command="date_picker_change"
|
|
type="text" id="date_from" name="date_from" class="span2 example"/>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
*/
|
|
|
|
// TODO(lsmola) write a proper doc for each attribute and method
|
|
horizon.d3_line_chart = {
|
|
LineChart: function(chart_class, html_element){
|
|
var self = this;
|
|
var jquery_element = $(html_element);
|
|
|
|
self.chart_class = chart_class;
|
|
self.html_element = html_element;
|
|
self.legend_element = $(jquery_element.data("legend-selector")).get(0);
|
|
self.slider_element = $(jquery_element.data("slider-selector")).get(0);
|
|
|
|
self.url = jquery_element.data("url");
|
|
self.url_parameters = jquery_element.data("url_parameters");
|
|
|
|
self.final_url = self.url;
|
|
if (jquery_element.data('form-selector')){
|
|
$(jquery_element.data('form-selector')).each(function(){
|
|
// Add serialized data from all connected forms to url.
|
|
if (self.final_url.indexOf('?') > -1){
|
|
self.final_url += '&' + $(this).serialize();
|
|
} else {
|
|
self.final_url += '?' + $(this).serialize();
|
|
}
|
|
});
|
|
}
|
|
|
|
self.data = []
|
|
self.color = d3.scale.category10();
|
|
|
|
self.load_settings = function(settings) {
|
|
// TODO (lsmola) make settings work
|
|
/* Settings will be obtained either from Json from server,
|
|
or from init of the charts, server will have priority */
|
|
self.settings = {};
|
|
self.settings.renderer = 'line';
|
|
self.settings.auto_size = true;
|
|
}
|
|
self.get_size = function(){
|
|
/* The height will be determined by css or window size,
|
|
I have to hide everything inside that could mess with
|
|
the size, so it is fully determined by outer CSS. */
|
|
$(self.html_element).css("height", "");
|
|
$(self.html_element).css("width", "");
|
|
var svg = $(self.html_element).find("svg");
|
|
svg.hide();
|
|
|
|
// Width an height of the chart will be taken from chart wrapper,
|
|
// that can be styled by css.
|
|
self.width = jquery_element.width();
|
|
|
|
// Set either the minimal height defined by CSS.
|
|
self.height = jquery_element.height();
|
|
/* Or stretch it to the remaining height of the window if there
|
|
is a place. + some space on the bottom, lets say 30px. */
|
|
if (self.settings.auto_size) {
|
|
var auto_height = $(window).height() - jquery_element.offset().top - 30;
|
|
if (auto_height > self.height) {
|
|
self.height = auto_height;
|
|
}
|
|
}
|
|
|
|
/* Setting new sizes. It is important when resizing a window.*/
|
|
$(self.html_element).css("height", self.height);
|
|
$(self.html_element).css("width", self.width);
|
|
svg.show();
|
|
svg.css("height", self.height);
|
|
svg.css("width", self.width);
|
|
}
|
|
// Load initial settings.
|
|
self.load_settings({});
|
|
// Get correct size of chart and the wrapper.
|
|
self.get_size();
|
|
|
|
self.refresh = function (){
|
|
var self = this;
|
|
|
|
self.start_loading();
|
|
horizon.ajax.queue({
|
|
url: self.final_url,
|
|
success: function (data, textStatus, jqXHR) {
|
|
// Clearing the old chart data.
|
|
$(self.html_element).html("");
|
|
$(self.legend_element).html("");
|
|
|
|
self.series = data.series;
|
|
self.load_settings(data.settings);
|
|
|
|
if (self.series.length <= 0) {
|
|
$(self.html_element).html("No data available.");
|
|
$(self.legend_element).html("");
|
|
// Setting a fix height breaks things when legend is getting
|
|
// bigger.
|
|
$(self.legend_element).css("height", "");
|
|
} else {
|
|
self.render();
|
|
}
|
|
},
|
|
error: function (jqXHR, textStatus, errorThrown) {
|
|
$(self.html_element).html("No data available.");
|
|
$(self.legend_element).html("");
|
|
// Setting a fix height breaks things when legend is getting
|
|
// bigger.
|
|
$(self.legend_element).css("height", "");
|
|
// FIXME add proper fail message
|
|
horizon.alert("error", gettext("An error occurred. Please try again later."));
|
|
},
|
|
complete: function (jqXHR, textStatus) {
|
|
self.finish_loading();
|
|
}
|
|
});
|
|
};
|
|
|
|
self.render = function(){
|
|
var self = this;
|
|
|
|
$.map(self.series, function (serie) {
|
|
serie.color = self.color(serie.name)
|
|
$.map(serie.data, function (statistic) {
|
|
// need to parse each date
|
|
statistic.x = d3.time.format("%Y-%m-%dT%H:%M:%S").parse(statistic.x);
|
|
statistic.x = statistic.x.getTime() / 1000;
|
|
});
|
|
});
|
|
|
|
// instantiate our graph!
|
|
var graph = new Rickshaw.Graph({
|
|
element: self.html_element,
|
|
width: self.width,
|
|
height: self.height,
|
|
renderer: self.settings.renderer,
|
|
series: self.series
|
|
});
|
|
|
|
/* TODO(lsmola) add JQuery UI slider to make this work
|
|
if (self.slider_element) {
|
|
var slider = new Rickshaw.Graph.RangeSlider({
|
|
graph: graph,
|
|
element: $(self.slider_element)
|
|
});
|
|
}*/
|
|
graph.render();
|
|
|
|
var hoverDetail = new Rickshaw.Graph.HoverDetail({
|
|
graph: graph,
|
|
formatter: function(series, x, y) {
|
|
var date = '<span class="date">' + new Date(x * 1000).toUTCString() + '</span>';
|
|
var swatch = '<span class="detail_swatch" style="background-color: ' + series.color + '"></span>';
|
|
var content = swatch + series.name + ": " + parseFloat(y).toFixed(2) + " " + series.unit + '<br>' + date;
|
|
return content;
|
|
}
|
|
});
|
|
|
|
if (self.legend_element) {
|
|
var legend = new Rickshaw.Graph.Legend({
|
|
graph: graph,
|
|
element: self.legend_element
|
|
});
|
|
|
|
var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({
|
|
graph: graph,
|
|
legend: legend
|
|
});
|
|
|
|
var order = new Rickshaw.Graph.Behavior.Series.Order({
|
|
graph: graph,
|
|
legend: legend
|
|
});
|
|
|
|
var highlighter = new Rickshaw.Graph.Behavior.Series.Highlight({
|
|
graph: graph,
|
|
legend: legend
|
|
});
|
|
}
|
|
var axes_x = new Rickshaw.Graph.Axis.Time({
|
|
graph: graph
|
|
});
|
|
axes_x.render();
|
|
|
|
var axes_y = new Rickshaw.Graph.Axis.Y({
|
|
graph: graph
|
|
});
|
|
axes_y.render();
|
|
|
|
/* Setting a fix height breaks things when chart is refreshed and
|
|
legend is getting bigger. */
|
|
$(self.legend_element).css("height", "");
|
|
};
|
|
self.start_loading = function () {
|
|
var self = this;
|
|
|
|
/* Find and remove backdrops and spinners that could be already there.*/
|
|
$(self.html_element).find(".modal-backdrop").remove();
|
|
$(self.html_element).find(".spinner_wrapper").remove();
|
|
|
|
// Display the backdrop that will be over the chart.
|
|
self.backdrop = $("<div class='modal-backdrop'></div>");
|
|
self.backdrop.css("width", self.width).css("height", self.height);
|
|
$(self.html_element).append(self.backdrop);
|
|
|
|
// Hide the legend.
|
|
$(self.legend_element).html("").addClass("disabled");
|
|
|
|
// Show the spinner.
|
|
self.spinner = $("<div class='spinner_wrapper'></div>");
|
|
$(self.html_element).append(self.spinner);
|
|
/* TODO(lsmola) a loader for in-line tables spark-lines has to be
|
|
prepared, the parameters of loader could be sent in settings. */
|
|
self.spinner.spin(horizon.conf.spinner_options.line_chart);
|
|
|
|
// Center the spinner considering the size of the spinner.
|
|
var radius = horizon.conf.spinner_options.line_chart.radius;
|
|
var length = horizon.conf.spinner_options.line_chart.length;
|
|
var spinner_size = radius + length;
|
|
var top = (self.height / 2) - spinner_size / 2;
|
|
var left = (self.width / 2) - spinner_size / 2;
|
|
self.spinner.css("top", top).css("left", left);
|
|
};
|
|
self.finish_loading = function () {
|
|
var self = this;
|
|
// Hiding the legend.
|
|
$(self.legend_element).removeClass("disabled");
|
|
};
|
|
},
|
|
init: function(selector, settings) {
|
|
var self = this;
|
|
$(selector).each(function() {
|
|
self.refresh(this);
|
|
});
|
|
|
|
/* I want to refresh chart on resize of the window, but only
|
|
at the end of the resize. Nice code from mr. Google. */
|
|
var rtime = new Date(1, 1, 2000, 12,00,00);
|
|
var timeout = false;
|
|
var delta = 400;
|
|
$(window).resize(function() {
|
|
rtime = new Date();
|
|
if (timeout === false) {
|
|
timeout = true;
|
|
setTimeout(resizeend, delta);
|
|
}
|
|
});
|
|
|
|
function resizeend() {
|
|
if (new Date() - rtime < delta) {
|
|
setTimeout(resizeend, delta);
|
|
} else {
|
|
timeout = false;
|
|
$(selector).each(function() {
|
|
self.refresh(this);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
self.bind_commands(selector);
|
|
},
|
|
refresh: function(html_element){
|
|
var chart = new this.LineChart(this, html_element)
|
|
// FIXME save chart objects somewhere so I can use them again when
|
|
// e.g. I am switching tabs, or if I want to update them
|
|
// via web sockets
|
|
// this.charts.add_or_update(chart)
|
|
chart.refresh();
|
|
},
|
|
bind_commands: function (selector){
|
|
// connecting controls of the charts
|
|
var select_box_selector = 'select[data-line-chart-command="select_box_change"]';
|
|
var datepicker_selector = 'input[data-line-chart-command="date_picker_change"]';
|
|
var self = this;
|
|
|
|
connect_forms_to_charts = function(){
|
|
/* Connect forms to charts. The charts contains information about
|
|
which form affects them. This information has to be projected to forms.
|
|
So when form input is changed, all connected charts are refreshed.
|
|
*/
|
|
$(selector).each(function() {
|
|
var chart = $(this);
|
|
$(chart.data('form-selector')).each(function(){
|
|
var form = $(this);
|
|
// each form is building a jquery selector for all charts it affects
|
|
var chart_identifier = 'div[data-form-selector="' + chart.data('form-selector') + '"]';
|
|
if (!form.data("charts_selector")){
|
|
form.data("charts_selector", chart_identifier);
|
|
} else {
|
|
form.data("charts_selector", form.data("charts_selector") + ", " + chart_identifier);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
delegate_event_and_refresh_charts = function(selector, event_name) {
|
|
$("form").delegate(selector, event_name, function() {
|
|
/* Registering 'any event' on form element by delegating. This way it can be easily overridden / enhanced
|
|
when some special functionality needs to be added. Like input element showing/hiding another
|
|
element on some condition will be defined directly on element and can block this default behavior.
|
|
*/
|
|
var invoker = $(this);
|
|
var form = invoker.parents("form").first();
|
|
|
|
$(form.data("charts_selector")).each(function(){
|
|
// refresh the chart connected to changed form
|
|
self.refresh(this);
|
|
});
|
|
});
|
|
};
|
|
bind_select_box_change = function(){
|
|
delegate_event_and_refresh_charts(select_box_selector, "change");
|
|
};
|
|
bind_datepicker_change = function (){
|
|
var now = new Date();
|
|
|
|
$(datepicker_selector).each(function() {
|
|
var el = $(this);
|
|
el.datepicker({format: "yyyy-mm-dd",
|
|
setDate: new Date(),
|
|
showButtonPanel: true})
|
|
});
|
|
delegate_event_and_refresh_charts(datepicker_selector, "changeDate");
|
|
};
|
|
connect_forms_to_charts();
|
|
bind_select_box_change();
|
|
bind_datepicker_change();
|
|
}
|
|
}
|
|
|
|
/* init the graphs */
|
|
horizon.addInitFunction(function () {
|
|
horizon.d3_line_chart.init("div[data-chart-type='line_chart']");
|
|
});
|