Add data view in observation with timeline and panels.
parent
b79dd8843b
commit
9ca0294572
|
@ -5,7 +5,7 @@ from django.core import serializers
|
|||
from django.http import HttpResponse
|
||||
|
||||
|
||||
from base.models import Station, Observation
|
||||
from base.models import Station, Observation, Data
|
||||
|
||||
|
||||
def index(request):
|
||||
|
@ -38,5 +38,6 @@ def stations_json(request):
|
|||
def view_observation(request, id):
|
||||
"""View for single observation page."""
|
||||
observation = get_object_or_404(Observation, id=id)
|
||||
data = Data.objects.filter(observation=observation)
|
||||
|
||||
return render(request, 'base/observation_view.html', {'observation': observation})
|
||||
return render(request, 'base/observation_view.html', {'observation': observation, 'data': data})
|
||||
|
|
|
@ -80,4 +80,29 @@
|
|||
#call2action .btn-lg {
|
||||
margin-left: 30px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.axis path,
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
.axis text {
|
||||
font-family: sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
.timeline-label {
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
#timeline2 .axis {
|
||||
transform: translate(0px,30px);
|
||||
-ms-transform: translate(0px,30px); /* IE 9 */
|
||||
-webkit-transform: translate(0px,30px); /* Safari and Chrome */
|
||||
-o-transform: translate(0px,30px); /* Opera */
|
||||
-moz-transform: translate(0px,30px); /* Firefox */
|
||||
}
|
||||
.coloredDiv {
|
||||
height:20px; width:20px; float:left;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,490 @@
|
|||
// vim: ts=2 sw=2
|
||||
(function () {
|
||||
d3.timeline = function() {
|
||||
var DISPLAY_TYPES = ["circle", "rect"];
|
||||
|
||||
var hover = function () {},
|
||||
mouseover = function () {},
|
||||
mouseout = function () {},
|
||||
click = function () {},
|
||||
scroll = function () {},
|
||||
orient = "bottom",
|
||||
width = null,
|
||||
height = null,
|
||||
rowSeperatorsColor = null,
|
||||
backgroundColor = null,
|
||||
tickFormat = { format: d3.time.format("%I %p"),
|
||||
tickTime: d3.time.hours,
|
||||
tickInterval: 1,
|
||||
tickSize: 6 },
|
||||
colorCycle = d3.scale.category20(),
|
||||
colorPropertyName = null,
|
||||
display = "rect",
|
||||
beginning = 0,
|
||||
ending = 0,
|
||||
margin = {left: 30, right:30, top: 30, bottom:30},
|
||||
stacked = false,
|
||||
rotateTicks = false,
|
||||
timeIsRelative = false,
|
||||
itemHeight = 20,
|
||||
itemMargin = 5,
|
||||
showTodayLine = false,
|
||||
showTodayFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle},
|
||||
showBorderLine = false,
|
||||
showBorderFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}
|
||||
;
|
||||
|
||||
function timeline (gParent) {
|
||||
var g = gParent.append("g");
|
||||
var gParentSize = gParent[0][0].getBoundingClientRect();
|
||||
|
||||
var gParentItem = d3.select(gParent[0][0]);
|
||||
|
||||
var yAxisMapping = {},
|
||||
maxStack = 1,
|
||||
minTime = 0,
|
||||
maxTime = 0;
|
||||
|
||||
setWidth();
|
||||
|
||||
// check if the user wants relative time
|
||||
// if so, substract the first timestamp from each subsequent timestamps
|
||||
if(timeIsRelative){
|
||||
g.each(function (d, i) {
|
||||
d.forEach(function (datum, index) {
|
||||
datum.times.forEach(function (time, j) {
|
||||
if(index === 0 && j === 0){
|
||||
originTime = time.starting_time; //Store the timestamp that will serve as origin
|
||||
time.starting_time = 0; //Set the origin
|
||||
time.ending_time = time.ending_time - originTime; //Store the relative time (millis)
|
||||
}else{
|
||||
time.starting_time = time.starting_time - originTime;
|
||||
time.ending_time = time.ending_time - originTime;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// check how many stacks we're gonna need
|
||||
// do this here so that we can draw the axis before the graph
|
||||
if (stacked || (ending === 0 && beginning === 0)) {
|
||||
g.each(function (d, i) {
|
||||
d.forEach(function (datum, index) {
|
||||
|
||||
// create y mapping for stacked graph
|
||||
if (stacked && Object.keys(yAxisMapping).indexOf(index) == -1) {
|
||||
yAxisMapping[index] = maxStack;
|
||||
maxStack++;
|
||||
}
|
||||
|
||||
// figure out beginning and ending times if they are unspecified
|
||||
if (ending === 0 && beginning === 0){
|
||||
datum.times.forEach(function (time, i) {
|
||||
if (time.starting_time < minTime || (minTime === 0 && timeIsRelative === false))
|
||||
minTime = time.starting_time;
|
||||
if (time.ending_time > maxTime)
|
||||
maxTime = time.ending_time;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (ending === 0 && beginning === 0) {
|
||||
beginning = minTime;
|
||||
ending = maxTime;
|
||||
}
|
||||
}
|
||||
|
||||
var scaleFactor = (1/(ending - beginning)) * (width - margin.left - margin.right);
|
||||
|
||||
// draw the axis
|
||||
var xScale = d3.time.scale()
|
||||
.domain([beginning, ending])
|
||||
.range([margin.left, width - margin.right]);
|
||||
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(xScale)
|
||||
.orient(orient)
|
||||
.tickFormat(tickFormat.format)
|
||||
.ticks(tickFormat.numTicks || tickFormat.tickTime, tickFormat.tickInterval)
|
||||
.tickSize(tickFormat.tickSize);
|
||||
|
||||
g.append("g")
|
||||
.attr("class", "axis")
|
||||
.attr("transform", "translate(" + 0 +","+(margin.top + (itemHeight + itemMargin) * maxStack)+")")
|
||||
.call(xAxis);
|
||||
|
||||
// draw the chart
|
||||
g.each(function(d, i) {
|
||||
d.forEach( function(datum, index){
|
||||
var data = datum.times;
|
||||
var hasLabel = (typeof(datum.label) != "undefined");
|
||||
var hasId = (typeof(datum.id) != "undefined");
|
||||
|
||||
|
||||
if (backgroundColor) {
|
||||
var greenbarYAxis = ((itemHeight + itemMargin) * yAxisMapping[index]);
|
||||
g.selectAll("svg").data(data).enter()
|
||||
.insert("rect")
|
||||
.attr("class", "row-green-bar")
|
||||
.attr("x", 0 + margin.left)
|
||||
.attr("width", width - margin.right - margin.left)
|
||||
.attr("y", greenbarYAxis)
|
||||
.attr("height", itemHeight)
|
||||
.attr("fill", backgroundColor)
|
||||
;
|
||||
}
|
||||
|
||||
g.selectAll("svg").data(data).enter()
|
||||
.append(display)
|
||||
.attr("x", getXPos)
|
||||
.attr("y", getStackPosition)
|
||||
.attr("width", function (d, i) {
|
||||
return (d.ending_time - d.starting_time) * scaleFactor;
|
||||
})
|
||||
.attr("cy", getStackPosition)
|
||||
.attr("cx", getXPos)
|
||||
.attr("r", itemHeight / 2)
|
||||
.attr("height", itemHeight)
|
||||
.style("fill", function(d, i){
|
||||
if (d.color) return d.color;
|
||||
if( colorPropertyName ){
|
||||
return colorCycle( datum[colorPropertyName] );
|
||||
}
|
||||
return colorCycle(index);
|
||||
})
|
||||
.on("mousemove", function (d, i) {
|
||||
hover(d, index, datum);
|
||||
})
|
||||
.on("mouseover", function (d, i) {
|
||||
mouseover(d, i, datum);
|
||||
})
|
||||
.on("mouseout", function (d, i) {
|
||||
mouseout(d, i, datum);
|
||||
})
|
||||
.on("click", function (d, i) {
|
||||
click(d, index, datum);
|
||||
})
|
||||
.attr("id", function (d, i) {
|
||||
if (hasId){
|
||||
return "timelineItem_"+datum.id;
|
||||
}else{
|
||||
return "timelineItem_"+index;
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
g.selectAll("svg").data(data).enter()
|
||||
.append("text")
|
||||
.attr("x", getXTextPos)
|
||||
.attr("y", getStackTextPosition)
|
||||
.text(function(d) {
|
||||
return d.label;
|
||||
})
|
||||
;
|
||||
|
||||
if (rowSeperatorsColor) {
|
||||
var lineYAxis = ( itemHeight + itemMargin / 2 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index]);
|
||||
gParent.append("svg:line")
|
||||
.attr("class", "row-seperator")
|
||||
.attr("x1", 0 + margin.left)
|
||||
.attr("x2", width - margin.right)
|
||||
.attr("y1", lineYAxis)
|
||||
.attr("y2", lineYAxis)
|
||||
.attr("stroke-width", 1)
|
||||
.attr("stroke", rowSeperatorsColor);
|
||||
;
|
||||
}
|
||||
|
||||
// add the label
|
||||
if (hasLabel) {
|
||||
gParent.append("text")
|
||||
.attr("class", "timeline-label")
|
||||
.attr("transform", "translate("+ 0 +","+ (itemHeight * 0.75 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")")
|
||||
.text(hasLabel ? datum.label : datum.id);
|
||||
}
|
||||
|
||||
if (typeof(datum.icon) !== "undefined") {
|
||||
gParent.append("image")
|
||||
.attr("class", "timeline-label")
|
||||
.attr("transform", "translate("+ 0 +","+ (margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")")
|
||||
.attr("xlink:href", datum.icon)
|
||||
.attr("width", margin.left)
|
||||
.attr("height", itemHeight);
|
||||
}
|
||||
|
||||
function getStackPosition(d, i) {
|
||||
if (stacked) {
|
||||
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index];
|
||||
}
|
||||
return margin.top;
|
||||
}
|
||||
function getStackTextPosition(d, i) {
|
||||
if (stacked) {
|
||||
return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75;
|
||||
}
|
||||
return margin.top + itemHeight * 0.75;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (width > gParentSize.width) {
|
||||
var move = function() {
|
||||
var x = Math.min(0, Math.max(gParentSize.width - width, d3.event.translate[0]));
|
||||
zoom.translate([x, 0]);
|
||||
g.attr("transform", "translate(" + x + ",0)");
|
||||
scroll(x*scaleFactor, xScale);
|
||||
};
|
||||
|
||||
var zoom = d3.behavior.zoom().x(xScale).on("zoom", move);
|
||||
|
||||
gParent
|
||||
.attr("class", "scrollable")
|
||||
.call(zoom);
|
||||
}
|
||||
|
||||
if (rotateTicks) {
|
||||
g.selectAll("text")
|
||||
.attr("transform", function(d) {
|
||||
return "rotate(" + rotateTicks + ")translate("
|
||||
+ (this.getBBox().width / 2 + 10) + "," // TODO: change this 10
|
||||
+ this.getBBox().height / 2 + ")";
|
||||
});
|
||||
}
|
||||
|
||||
var gSize = g[0][0].getBoundingClientRect();
|
||||
setHeight();
|
||||
|
||||
if (showBorderLine) {
|
||||
g.each(function (d, i) {
|
||||
d.forEach(function (datum) {
|
||||
var times = datum.times;
|
||||
times.forEach(function (time) {
|
||||
appendLine(xScale(time.starting_time), showBorderFormat);
|
||||
appendLine(xScale(time.ending_time), showBorderFormat);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (showTodayLine) {
|
||||
var todayLine = xScale(new Date());
|
||||
appendLine(todayLine, showTodayFormat);
|
||||
}
|
||||
|
||||
function getXPos(d, i) {
|
||||
return margin.left + (d.starting_time - beginning) * scaleFactor;
|
||||
}
|
||||
|
||||
function getXTextPos(d, i) {
|
||||
return margin.left + (d.starting_time - beginning) * scaleFactor + 5;
|
||||
}
|
||||
|
||||
function setHeight() {
|
||||
if (!height && !gParentItem.attr("height")) {
|
||||
if (itemHeight) {
|
||||
// set height based off of item height
|
||||
height = gSize.height + gSize.top - gParentSize.top;
|
||||
// set bounding rectangle height
|
||||
d3.select(gParent[0][0]).attr("height", height);
|
||||
} else {
|
||||
throw "height of the timeline is not set";
|
||||
}
|
||||
} else {
|
||||
if (!height) {
|
||||
height = gParentItem.attr("height");
|
||||
} else {
|
||||
gParentItem.attr("height", height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWidth() {
|
||||
if (!width && !gParentSize.width) {
|
||||
try {
|
||||
width = gParentItem.attr("width");
|
||||
if (!width) {
|
||||
throw "width of the timeline is not set. As of Firefox 27, timeline().with(x) needs to be explicitly set in order to render";
|
||||
}
|
||||
} catch (err) {
|
||||
console.log( err );
|
||||
}
|
||||
} else if (!(width && gParentSize.width)) {
|
||||
try {
|
||||
width = gParentItem.attr("width");
|
||||
} catch (err) {
|
||||
console.log( err );
|
||||
}
|
||||
}
|
||||
// if both are set, do nothing
|
||||
}
|
||||
|
||||
function appendLine(lineScale, lineFormat) {
|
||||
gParent.append("svg:line")
|
||||
.attr("x1", lineScale)
|
||||
.attr("y1", lineFormat.marginTop)
|
||||
.attr("x2", lineScale)
|
||||
.attr("y2", height - lineFormat.marginBottom)
|
||||
.style("stroke", lineFormat.color)//"rgb(6,120,155)")
|
||||
.style("stroke-width", lineFormat.width);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SETTINGS
|
||||
|
||||
timeline.margin = function (p) {
|
||||
if (!arguments.length) return margin;
|
||||
margin = p;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.orient = function (orientation) {
|
||||
if (!arguments.length) return orient;
|
||||
orient = orientation;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.itemHeight = function (h) {
|
||||
if (!arguments.length) return itemHeight;
|
||||
itemHeight = h;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.itemMargin = function (h) {
|
||||
if (!arguments.length) return itemMargin;
|
||||
itemMargin = h;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.height = function (h) {
|
||||
if (!arguments.length) return height;
|
||||
height = h;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.width = function (w) {
|
||||
if (!arguments.length) return width;
|
||||
width = w;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.display = function (displayType) {
|
||||
if (!arguments.length || (DISPLAY_TYPES.indexOf(displayType) == -1)) return display;
|
||||
display = displayType;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.tickFormat = function (format) {
|
||||
if (!arguments.length) return tickFormat;
|
||||
tickFormat = format;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.hover = function (hoverFunc) {
|
||||
if (!arguments.length) return hover;
|
||||
hover = hoverFunc;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.mouseover = function (mouseoverFunc) {
|
||||
if (!arguments.length) return mouseoverFunc;
|
||||
mouseover = mouseoverFunc;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.mouseout = function (mouseoverFunc) {
|
||||
if (!arguments.length) return mouseoverFunc;
|
||||
mouseout = mouseoverFunc;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.click = function (clickFunc) {
|
||||
if (!arguments.length) return click;
|
||||
click = clickFunc;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.scroll = function (scrollFunc) {
|
||||
if (!arguments.length) return scroll;
|
||||
scroll = scrollFunc;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.colors = function (colorFormat) {
|
||||
if (!arguments.length) return colorCycle;
|
||||
colorCycle = colorFormat;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.beginning = function (b) {
|
||||
if (!arguments.length) return beginning;
|
||||
beginning = b;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.ending = function (e) {
|
||||
if (!arguments.length) return ending;
|
||||
ending = e;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.rotateTicks = function (degrees) {
|
||||
rotateTicks = degrees;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.stack = function () {
|
||||
stacked = !stacked;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.relativeTime = function() {
|
||||
timeIsRelative = !timeIsRelative;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.showBorderLine = function () {
|
||||
showBorderLine = !showBorderLine;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.showBorderFormat = function(borderFormat) {
|
||||
if (!arguments.length) return showBorderFormat;
|
||||
showBorderFormat = borderFormat;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.showToday = function () {
|
||||
showTodayLine = !showTodayLine;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.showTodayFormat = function(todayFormat) {
|
||||
if (!arguments.length) return showTodayFormat;
|
||||
showTodayFormat = todayFormat;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.colorProperty = function(colorProp) {
|
||||
if (!arguments.length) return colorPropertyName;
|
||||
colorPropertyName = colorProp;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.rowSeperators = function (color) {
|
||||
if (!arguments.length) return rowSeperatorsColor;
|
||||
rowSeperatorsColor = color;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
timeline.background = function (color) {
|
||||
if (!arguments.length) return backgroundColor;
|
||||
backgroundColor = color;
|
||||
return timeline;
|
||||
};
|
||||
|
||||
return timeline;
|
||||
};
|
||||
})();
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,31 @@
|
|||
observation_start = 1000 * $('#observation-info').data('start');
|
||||
observation_end = 1000 * $('#observation-info').data('end');
|
||||
|
||||
var observation_data = [];
|
||||
|
||||
$('.observation-data').each(function( index ){
|
||||
var data_groundstation = $(this).data('groundstation');
|
||||
var data_time_start = 1000 * $(this).data('start');
|
||||
var data_time_end = 1000 * $(this).data('end');
|
||||
observation_data.push({label : data_groundstation, times : [{starting_time: data_time_start, ending_time: data_time_end}]});
|
||||
});
|
||||
|
||||
var chart = d3.timeline()
|
||||
.stack()
|
||||
.beginning(observation_start)
|
||||
.ending(observation_end)
|
||||
.hover(function (d, i, datum) {
|
||||
// d is the current rendering object
|
||||
// i is the index during d3 rendering
|
||||
// datum is the id object
|
||||
var div = $('#hoverRes');
|
||||
var colors = chart.colors();
|
||||
div.find('.coloredDiv').css('background-color', colors(i))
|
||||
div.find('#name').text(datum.label);
|
||||
})
|
||||
.margin({left:140, right:10, top:0, bottom:50})
|
||||
.tickFormat({format: d3.time.format("%H:%M"), tickTime: d3.time.minutes, tickInterval: 30, tickSize: 6})
|
||||
;
|
||||
|
||||
var svg = d3.select("#timeline").append("svg").attr("width", 1140)
|
||||
.datum(observation_data).call(chart);
|
|
@ -1,15 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load staticfiles i18n %}
|
||||
|
||||
{% block title %}Observation {{ observation.id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Observation #{{ observation.id }}</h1>
|
||||
<h2 id="observation-info"
|
||||
data-start="{{ observation.start|date:"U"}}"
|
||||
data-end="{{ observation.end|date:"U"}}">
|
||||
Observation #{{ observation.id }}
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Satellite</th>
|
||||
<th>Frequency</th>
|
||||
<th>Encoding</th>
|
||||
|
@ -18,24 +23,68 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="label
|
||||
{% if not observation.data_set.all %}
|
||||
label-danger
|
||||
{% else %}
|
||||
label-success
|
||||
{% endif %}">
|
||||
{{ observation.id }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ observation.satellite.name }}</td>
|
||||
<td>{{ observation.satellite.norad_cat_id }} - {{ observation.satellite.name }}</td>
|
||||
<td>{{ observation.transponder.downlink_low }}</td>
|
||||
<td>{{ observation.transponder.mode }}</td>
|
||||
<td>{{ observation.start|date:"Y-m-d H:i:s" }}</br>{{ observation.end|date:"Y-m-d H:i:s" }}</td>
|
||||
<td>{{ observation.author.get_full_name }}</td>
|
||||
<td>
|
||||
<a href="{% url 'users:detail' observation.author.username %}">
|
||||
{{ observation.author.get_full_name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Timeline</h3>
|
||||
<div id="timeline"></div>
|
||||
<div id="hoverRes">
|
||||
<div class="coloredDiv"></div>
|
||||
<div id="name"></div>
|
||||
<div id="scrolled_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Data</h3>
|
||||
{% for data in data %}
|
||||
<div class="panel panel-default observation-data"
|
||||
data-id="{{ data.id }}"
|
||||
data-url="{{ data.url }}"
|
||||
data-start="{{ data.start|date:"U" }}"
|
||||
data-end="{{ data.end|date:"U" }}"
|
||||
data-groundstation="{{ data.ground_station }}">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Data payload #{{ data.id }} from {{ data.ground_station }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
Data view goes here.
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="label label-default">Timeframe</span>
|
||||
{{ data.start|date:"Y-m-d H:i:s" }} ~ {{ data.end|date:"Y-m-d H:i:s" }}
|
||||
<a href="{{ data.url }}" target="_blank" class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-download"></span> Data
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/d3.v3.min.js' %}"></script>
|
||||
<script src="{% static 'js/d3-timeline.js' %}"></script>
|
||||
<script src="{% static 'js/observation_view.js' %}"></script>
|
||||
{% endblock javascript %}
|
Loading…
Reference in New Issue