Merge pull request #111 from satnogs/i88-telemetry-visualisation
Telemetry visualisationpull/112/head
commit
842c12a240
|
@ -10,6 +10,8 @@
|
|||
"underscore": "1.8.x",
|
||||
"backbone": "1.3.x",
|
||||
"d3": "3.5.x",
|
||||
"chart.js": "^2.4.0"
|
||||
"chart.js": "^2.4.0",
|
||||
"moment": "2.10.6",
|
||||
"bootstrap-daterangepicker": "2.1.x"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,6 +256,7 @@ footer {
|
|||
color: #ffff00;
|
||||
}
|
||||
|
||||
|
||||
/* Statistics page
|
||||
==================== */
|
||||
.stats {
|
||||
|
@ -272,3 +273,80 @@ footer {
|
|||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* Telemetry D3 visualisation
|
||||
============================= */
|
||||
|
||||
.chart text {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
svg.chart {
|
||||
margin: 10px 50px;
|
||||
}
|
||||
|
||||
#telemetry-descriptors {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.telemetry-key {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circle {
|
||||
fill: #286090;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart-tooltip {
|
||||
position: absolute;
|
||||
background: #286090;
|
||||
color: #fff;
|
||||
font-family: 'ClearSans';
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 5px 5px 6px 5px;
|
||||
border: 0px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tick line, .domain {
|
||||
fill: none;
|
||||
stroke: #ddd;
|
||||
}
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
stroke: #286090;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* Telemetry Datepicker */
|
||||
|
||||
.datepicker {
|
||||
border-radius: 3px;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.datepicker input {
|
||||
font-family: 'ClearSans';
|
||||
font-size: 13px;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
box-shadow: inset 0px 0px 8px rgba(0,0,0,0.3);
|
||||
border-bottom-right-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepicker .input-group-addon {
|
||||
background: #777;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.daterangepicker {
|
||||
font-size: 13px;
|
||||
}
|
|
@ -1,86 +1,46 @@
|
|||
// Retreive Satellite Id
|
||||
// D3 visualisation
|
||||
|
||||
var satelliteId = $('#telemetry-block').data('satid');
|
||||
|
||||
// Models
|
||||
|
||||
var Telemetry = Backbone.Model.extend({});
|
||||
|
||||
var TelemetryData = Backbone.Model.extend({
|
||||
url:"/api/telemetry/?satellite=" + satelliteId,
|
||||
defaults: {
|
||||
data: [],
|
||||
dimension: {},
|
||||
config: {height: 500, width: 700}
|
||||
},
|
||||
parse: function(_json) {
|
||||
var data = _json;
|
||||
this.set({data: data});
|
||||
},
|
||||
});
|
||||
|
||||
// Collections
|
||||
|
||||
var TelemetryCollection = Backbone.Collection.extend({
|
||||
url:"/api/telemetry/?satellite=" + satelliteId
|
||||
});
|
||||
|
||||
var TelemetryDescriptors = TelemetryCollection.extend({
|
||||
parse: function(response){
|
||||
return response[0].appendix;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Views
|
||||
|
||||
var TelemetryDescriptorsView = Backbone.View.extend({
|
||||
el: "#telemetry-descriptors",
|
||||
template: _.template($('#telemetryDescriptorsTemplate').html()),
|
||||
initialize: function(){
|
||||
this.listenTo(this.collection, 'add reset change remove', this.renderItem);
|
||||
this.collection.fetch();
|
||||
},
|
||||
render: function () {
|
||||
this.collection.each(function(model){
|
||||
this.$el.append(this.template(model.toJSON()));
|
||||
}, this);
|
||||
return this;
|
||||
},
|
||||
renderItem: function (model) {
|
||||
this.$el.append(this.template(model.toJSON()));
|
||||
}
|
||||
});
|
||||
|
||||
// D3 Visualisation
|
||||
|
||||
d3.custom = {};
|
||||
|
||||
d3.custom.barChart = function module(telemetry_key) {
|
||||
d3.lineChart = function(telemetry_key, unit) {
|
||||
var config = {
|
||||
margin: {top: 20, right: 20, bottom: 60, left: 60},
|
||||
margin: {top: 20, right: 20, bottom: 115, left: 100},
|
||||
width: 700,
|
||||
height: 500
|
||||
};
|
||||
|
||||
var svg;
|
||||
|
||||
var dispatch = d3.dispatch('customHover');
|
||||
// Define the div for the tooltip
|
||||
var div = d3.select("body").append("div")
|
||||
.attr("class", "chart-tooltip")
|
||||
.style("opacity", 0);
|
||||
|
||||
function exports(_selection) {
|
||||
_selection.each(function(_data) {
|
||||
function render(selection) {
|
||||
selection.each(function(_data) {
|
||||
var chartW = config.width - config.margin.left - config.margin.right,
|
||||
chartH = config.height - config.margin.top - config.margin.bottom;
|
||||
|
||||
|
||||
var x1 = d3.scale.ordinal()
|
||||
.domain(_data.map(function(d, i){
|
||||
return i ;
|
||||
return parseDate(d.telemetry.observation_datetime);
|
||||
}))
|
||||
.rangeRoundBands([0, chartW], 0.1);
|
||||
.rangePoints([0, chartW]);
|
||||
|
||||
var y1 = d3.scale.linear()
|
||||
.domain([0, d3.max(_data, function(d, i){ return +d.telemetry.damod_data[telemetry_key]; })])
|
||||
.range([chartH, 0])
|
||||
.nice(4);
|
||||
var y1;
|
||||
|
||||
switch(_data.length) {
|
||||
case 1:
|
||||
y1 = d3.scale.linear()
|
||||
.domain([0, d3.max(_data, function(d, i){ return +d.telemetry.damod_data[telemetry_key]; })])
|
||||
.range([chartH, 0])
|
||||
.nice(4);
|
||||
break;
|
||||
default:
|
||||
y1 = d3.scale.linear()
|
||||
.domain(d3.extent(_data, function(d, i){ return +d.telemetry.damod_data[telemetry_key]; }))
|
||||
.range([chartH, 0])
|
||||
.nice(4);
|
||||
}
|
||||
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(x1)
|
||||
|
@ -90,12 +50,10 @@ d3.custom.barChart = function module(telemetry_key) {
|
|||
.scale(y1)
|
||||
.orient('left');
|
||||
|
||||
var xInterval = chartW / _data.length;
|
||||
|
||||
if(!svg) {
|
||||
svg = d3.select(this)
|
||||
.append('svg')
|
||||
.classed('chart', true);
|
||||
.classed('svg-chart', true);
|
||||
var container = svg.append('g').classed('container-group', true);
|
||||
container.append('g').classed('chart-group', true);
|
||||
container.append('g').classed('x-axis-group axis', true);
|
||||
|
@ -103,6 +61,7 @@ d3.custom.barChart = function module(telemetry_key) {
|
|||
}
|
||||
|
||||
svg.transition().attr({width: config.width, height: config.height});
|
||||
|
||||
svg.select('.container-group')
|
||||
.attr({transform: 'translate(' + config.margin.left + ',' + config.margin.top + ')'});
|
||||
|
||||
|
@ -115,92 +74,218 @@ d3.custom.barChart = function module(telemetry_key) {
|
|||
.transition()
|
||||
.call(yAxis);
|
||||
|
||||
svg.selectAll(".x-axis-group.axis text") // select all the text elements for the xaxis
|
||||
.attr("transform", function(d) {
|
||||
return "translate(-50,50)rotate(-45)";
|
||||
});
|
||||
|
||||
// Axis labels
|
||||
svg.append("text")
|
||||
.attr("transform", "translate(" + (chartW / 2) + " ," + (chartH + config.margin.bottom + config.margin.top) + ")")
|
||||
.attr("transform", "translate(" + (chartW + config.margin.right + 18) + " ," + (chartH + 10) + ")")
|
||||
.style("text-anchor", "middle")
|
||||
.text("Observation Datetime");
|
||||
|
||||
svg.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", 0)
|
||||
.attr("y", 40)
|
||||
.attr("x", 0 - (chartH / 2))
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.text("Value");
|
||||
.text("Value (" + unit + ")");
|
||||
|
||||
// Define the line
|
||||
var valueline = d3.svg.line()
|
||||
.x(function(d,i) { return (xInterval*i + config.margin.left); })
|
||||
.y(function(d) { return y1(d.telemetry.damod_data[telemetry_key]) + config.margin.top; });
|
||||
switch(_data.length) {
|
||||
case 1:
|
||||
// Add the scatterplot
|
||||
svg.selectAll("dot")
|
||||
.data(_data)
|
||||
.enter().append("circle")
|
||||
.attr("r", 4)
|
||||
.attr("cx", function(d, i) { return chartW / 2 + config.margin.left; })
|
||||
.attr("cy", function(d) { return y1(d.telemetry.damod_data[telemetry_key]) + config.margin.top; })
|
||||
.attr("class", "circle")
|
||||
.on("mouseover", function(d) {
|
||||
div.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1);
|
||||
div.html(d.telemetry.damod_data[telemetry_key] + ' (' + unit + ')')
|
||||
.style("left", (d3.event.pageX) + "px")
|
||||
.style("top", (d3.event.pageY - 26) + "px");
|
||||
})
|
||||
.on("mouseout", function(d) {
|
||||
div.transition()
|
||||
.duration(500)
|
||||
.style("opacity", 0);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
var xInterval = chartW / (_data.length - 1);
|
||||
|
||||
// Add the valueline path
|
||||
svg.append("path")
|
||||
.attr("class", "line")
|
||||
.attr("d", valueline(_data));
|
||||
// Define the line
|
||||
var valueline = d3.svg.line()
|
||||
.x(function(d,i) { return (xInterval*i + config.margin.left); })
|
||||
.y(function(d) { return y1(d.telemetry.damod_data[telemetry_key]) + config.margin.top; });
|
||||
|
||||
// Add the scatterplot
|
||||
svg.selectAll("dot")
|
||||
.data(_data)
|
||||
.enter().append("circle")
|
||||
.attr("r", 3.5)
|
||||
.attr("cx", function(d, i) { return xInterval*i + config.margin.left; })
|
||||
.attr("cy", function(d) { return y1(d.telemetry.damod_data[telemetry_key]) + config.margin.top; });
|
||||
// Add the valueline path
|
||||
svg.append("path")
|
||||
.attr("class", "line")
|
||||
.attr("d", valueline(_data));
|
||||
|
||||
// Add the scatterplot
|
||||
svg.selectAll("dot")
|
||||
.data(_data)
|
||||
.enter().append("circle")
|
||||
.attr("r", 4)
|
||||
.attr("cx", function(d, i) { return xInterval*i + config.margin.left; })
|
||||
.attr("cy", function(d) { return y1(d.telemetry.damod_data[telemetry_key]) + config.margin.top; })
|
||||
.attr("class", "circle")
|
||||
.on("mouseover", function(d) {
|
||||
div.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1);
|
||||
div.html(d.telemetry.damod_data[telemetry_key] + ' (' + unit + ')')
|
||||
.style("left", (d3.event.pageX) + "px")
|
||||
.style("top", (d3.event.pageY - 26) + "px");
|
||||
})
|
||||
.on("mouseout", function(d) {
|
||||
div.transition()
|
||||
.duration(500)
|
||||
.style("opacity", 0);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
exports.config = function(_newConfig) {
|
||||
if (!arguments.length) return width;
|
||||
for(var x in _newConfig) if(x in config) config[x] = _newConfig[x];
|
||||
return this;
|
||||
};
|
||||
d3.rebind(exports, dispatch, 'on');
|
||||
return exports;
|
||||
return render;
|
||||
};
|
||||
|
||||
// Retreive Satellite Id
|
||||
|
||||
function parseDate (date) {
|
||||
var res = date.substring(9 ,11) + ':' + date.substring(11 ,13) + ':' + date.substring(13 ,15) + ' ' + date.substring(4,6) + '/' + date.substring(6,8) + '/' + date.substring(0,4);
|
||||
return res;
|
||||
}
|
||||
var satelliteId = $('#telemetry-block').data('satid');
|
||||
|
||||
var TelemetryVizView = Backbone.View.extend({
|
||||
// Backbone Models
|
||||
|
||||
var TelemetryData = Backbone.Model.extend({});
|
||||
|
||||
// Backbone Collections
|
||||
|
||||
var TelemetryCollection = Backbone.Collection.extend({
|
||||
url:"/api/telemetry/?satellite=" + satelliteId
|
||||
});
|
||||
|
||||
var TelemetryDescriptors = TelemetryCollection.extend({
|
||||
parse: function(response){
|
||||
if(response.length !== 0) {
|
||||
return response[0].appendix;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var TelemetryValues = TelemetryCollection.extend({
|
||||
comparator: function(collection){
|
||||
return( collection.get('telemetry').observation_datetime );
|
||||
},
|
||||
byDate: function (start_date, end_date) {
|
||||
filtered = this.filter(function (model) {
|
||||
var date = parseDateFilter(model.get('telemetry').observation_datetime);
|
||||
return ( date >= start_date && date <= end_date );
|
||||
});
|
||||
return new TelemetryValues(filtered);
|
||||
}
|
||||
});
|
||||
|
||||
// Backbone Views
|
||||
|
||||
var TelemetryDescriptorsView = Backbone.View.extend({
|
||||
el: "#telemetry-descriptors",
|
||||
template: _.template($('#telemetryDescriptorsTemplate').html()),
|
||||
initialize: function(){
|
||||
this.listenTo(this.collection, 'add reset change remove', this.renderItem);
|
||||
this.collection.fetch();
|
||||
},
|
||||
renderItem: function (model) {
|
||||
this.$el.append(this.template(model.toJSON()));
|
||||
$('#telemetry-descriptors li:first-child').addClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
var TelemetryChartView = Backbone.View.extend({
|
||||
el: ".chart",
|
||||
chart: null,
|
||||
chartSelection: null,
|
||||
initialize: function() {
|
||||
var that = this;
|
||||
this.model.fetch();
|
||||
_.bindAll(this, 'render', 'update');
|
||||
this.model.bind('change:data', this.render);
|
||||
this.model.bind('change:config', this.update);
|
||||
chart = d3.custom.barChart();
|
||||
chart.config(this.model.get('config'));
|
||||
chart.on('customHover', function(d, i){ console.log('hover', d, i); });
|
||||
this.collection.fetch();
|
||||
this.updateDates(moment().subtract(7, 'days').format('YYYY/MM/DD'), moment().format('YYYY/MM/DD'));
|
||||
this.renderPlaceholder();
|
||||
this.collection.on('update filter', this.render, this);
|
||||
chart = d3.lineChart();
|
||||
},
|
||||
events: {
|
||||
"click .telemetry-key": "update",
|
||||
},
|
||||
renderPlaceholder: function() {
|
||||
this.chartSelection = d3.select(this.el)
|
||||
.datum([{key: '', value: 0}])
|
||||
.call(d3.custom.barChart(this.model.get('data')[0].appendix[1].key));
|
||||
"click .telemetry-key": "updateKey",
|
||||
},
|
||||
render: function() {
|
||||
this.chartSelection = d3.select(this.el)
|
||||
.datum(this.model.get('data'))
|
||||
.call(d3.custom.barChart(this.model.get('data')[0].appendix[1].key));
|
||||
if (this.collection.length > 0) {
|
||||
$('#telemetry-descriptors').show();
|
||||
$('#data-available').empty();
|
||||
d3.select('svg').remove();
|
||||
var data = this.collection.toJSON();
|
||||
this.chartSelection = d3.select(this.el)
|
||||
.datum(data)
|
||||
.call(d3.lineChart(data[0].appendix[0].key, data[0].appendix[0].unit));
|
||||
} else {
|
||||
this.renderPlaceholder();
|
||||
}
|
||||
},
|
||||
update: function(e){
|
||||
d3.select("svg").remove();
|
||||
this.chartSelection.call(d3.custom.barChart($(e.currentTarget).attr('id')));
|
||||
renderPlaceholder: function() {
|
||||
$('#telemetry-descriptors').hide();
|
||||
$('#data-available').html("<p>There is no data available for the selected dates.</p>");
|
||||
d3.select('svg').remove();
|
||||
},
|
||||
updateKey: function(e){
|
||||
d3.select('svg').remove();
|
||||
this.chartSelection.call(d3.lineChart($(e.currentTarget).data("key"), $(e.currentTarget).data("unit")));
|
||||
var active = $(e.currentTarget);
|
||||
active.addClass('active');
|
||||
$('li').not(active).removeClass('active');
|
||||
},
|
||||
updateDates: function(start_date, end_date){
|
||||
this.collection = telemetryValues.byDate(start_date, end_date);
|
||||
this.render();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Fetch data and render views
|
||||
|
||||
var telemetryDescriptorsView = new TelemetryDescriptorsView({ collection: new TelemetryDescriptors() });
|
||||
var telemetryDataModel = new TelemetryData();
|
||||
var telemetryVizView = new TelemetryVizView({model: telemetryDataModel});
|
||||
var telemetryValues = new TelemetryValues();
|
||||
var telemetryChartView = new TelemetryChartView({collection: telemetryValues});
|
||||
|
||||
|
||||
// Parse datetime values
|
||||
|
||||
function parseDate (date) {
|
||||
var res = date.substring(4,6) + '/' + date.substring(6,8) + '/' + date.substring(0,4) + ' ' + date.substring(9 ,11) + ':' + date.substring(11 ,13) + ':' + date.substring(13 ,15);
|
||||
return res;
|
||||
}
|
||||
|
||||
function parseDateFilter (date) {
|
||||
var res = date.substring(0,8);
|
||||
return res;
|
||||
}
|
||||
|
||||
$('input[name="daterange"]').daterangepicker(
|
||||
{
|
||||
locale: {
|
||||
format: 'YYYY/MM/DD'
|
||||
},
|
||||
dateLimit: {
|
||||
"days": 60
|
||||
},
|
||||
autoApply: true,
|
||||
startDate: moment().subtract(7, 'days').format('YYYY/MM/DD'),
|
||||
endDate: moment().format('YYYY/MM/DD'),
|
||||
},
|
||||
function(start, end, label) {
|
||||
telemetryChartView.updateDates(start.format('YYYYMMDD'), end.format('YYYYMMDD'));
|
||||
}
|
||||
);
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
{% block title %} - Suggest{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static 'lib/bootstrap-daterangepicker/daterangepicker.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block top %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -260,7 +264,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Telemetry Section -->
|
||||
<div class="row">
|
||||
<div id="telemetry" class="row hidden-xs hidden-sm">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 satellite-panels">
|
||||
<div class="panel panel-default panel-satellite">
|
||||
<div class="panel-heading">
|
||||
|
@ -271,10 +275,14 @@
|
|||
<div class="row panel-body">
|
||||
<div class="panel-satellite chart">
|
||||
<div class="col-md-3 panel-satellite">
|
||||
<div class="input-group datepicker">
|
||||
<span class="input-group-addon">Date range</span>
|
||||
<input type="text" name="daterange"/>
|
||||
</div>
|
||||
<ul id="telemetry-descriptors">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-9 col-sm-9 col-xs-12">
|
||||
<div id="data-available" class="text-danger">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -283,7 +291,7 @@
|
|||
</div>
|
||||
|
||||
<script id="telemetryDescriptorsTemplate" type="text/template">
|
||||
<li class="list-group-item active telemetry-key" id="<%- key %>"><%- description %></li>
|
||||
<li class="list-group-item telemetry-key" data-key="<%- key %>" data-unit="<%- unit %>"><%- description %></li>
|
||||
</script>
|
||||
|
||||
<!-- Suggestion Modal -->
|
||||
|
@ -370,5 +378,7 @@
|
|||
<script src="{% static 'lib/underscore/underscore-min.js' %}"></script>
|
||||
<script src="{% static 'lib/backbone/backbone-min.js' %}"></script>
|
||||
<script src="{% static 'lib/d3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'lib/moment/moment.js' %}"></script>
|
||||
<script src="{% static 'lib/bootstrap-daterangepicker/daterangepicker.js' %}"></script>
|
||||
<script src="{% static 'js/app.backbone.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in New Issue