Skip to content

Commit

Permalink
Merge a4fe18f into 727ccc8
Browse files Browse the repository at this point in the history
  • Loading branch information
dougthor42 committed May 13, 2019
2 parents 727ccc8 + a4fe18f commit 5621893
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 83 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
+ Links to the Swagger/ReDoc API documentation are now provided on the main
page. (#152)
+ Fixed small error in development documentation. (#153)
+ Plots are now all shown on a single page, thus making it much easier to
navigate between plots. (#58)
+ Added Bootstrap4
+ updated the jstree data structure to use internal ID instead of a
generated URL (#156)


## 0.6.0b1 (2019-05-01)
Expand Down
64 changes: 39 additions & 25 deletions src/trendlines/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,35 +54,35 @@ class Meta:
model = orm.DataPoint


@pages.route('/', methods=['GET'])
def index():
@pages.route("/", methods=['GET'])
@pages.route("/plot/<metric>", methods=["GET"])
def index(metric=None):
"""
Main page.
Displays a list of all the known metrics with links.
"""
raw_data = db.get_metrics()
data = utils.build_jstree_data(raw_data)
return render_template("trendlines/index.html", data=data)

Also allows direct links to a specific plot.
@pages.route("/plot/<metric>", methods=["GET"])
def plot(metric=None):
"""
Plot a given metric.
Parameters
----------
metric : str or int, optional
The metric_id or metric name to plot.
"""
if metric is None:
return "Need a metric, friend!"
metric_name = None
if metric is not None:
# Support both metric_id and metric_name
try:
metric_id = int(metric)
metric_name = orm.Metric.get(orm.Metric.metric_id == metric_id).name
except ValueError:
# We couldn't parse as an int, so it's a metric name instead.
metric_name = metric

data = db.get_data(metric)
units = db.get_units(metric)
data = utils.format_data(data, units)
if len(data) == 0:
logger.warning("No data exists for metric '%s'" % metric)
return "Metric '{}' wasn't found. No data, maybe?".format(metric)
metric_list = db.get_metrics()
tree_data = utils.build_jstree_data(metric_list)

# TODO: Ajax request for this data instead of sending it to the template.
return render_template('trendlines/plot.html', name=metric, data=data)
return render_template('trendlines/index.html',
tree_data=tree_data,
metric_id=metric_name)


@api.route("/api/v1/data")
Expand Down Expand Up @@ -117,13 +117,27 @@ def post(self):
return msg, 201


@api.route("/api/v1/data/<metric_name>")
@api.route("/api/v1/data/<metric>")
class DataByName(MethodView):
def get(self, metric_name):
def get(self, metric):
"""
Return data for a given metric as JSON.
Parameters
----------
metric : str or int
The metric name or the metric internal id (int) to get data for.
"""
logger.debug("API: get '%s'" % metric_name)
logger.debug("GET /api/v1/data/%s" % metric)

# Support both metric_id and metric_name
try:
metric_id = int(metric)
metric_name = orm.Metric.get(orm.Metric.metric_id == metric_id).name
except ValueError:
# We couldn't parse as an int, so it's a metric name instead.
metric_name = metric

try:
raw_data = db.get_data(metric_name)
units = db.get_units(metric_name)
Expand Down
68 changes: 54 additions & 14 deletions src/trendlines/static/core.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* Populate the JSTree tree.
*/
function populateTree(data) {
function populateTree(data, metricId) {
var tree = $('#jstree-div');

// Create an instance when the DOM is ready.
tree.jstree(
{
Expand All @@ -13,37 +14,76 @@ function populateTree(data) {
);

// Bind events.
tree.on("ready.jstree", function(e, data){
console.log("tree ready");
selectNodeById(tree, metricId);
});

tree.on("changed.jstree", function (e, data) {
});

tree.on("loaded.jstree", function () {
tree.jstree('open_all');
});

// Go to data pages if they exist, otherwise just open the tree.
tree.on('select_node.jstree', function(e, data) {
// jsTree puts the original data structure in a nested object
// called 'original'. How original of them. Hahaha I crack myself up.
// If `url` is defined, take us there.
if (data.node.original.metric_id !== null) {
var expected = "/plot/" + data.node.original.id;
var new_href = document.location.origin + expected;

// Take the user to the plot page.
document.location.href = new_href;
} else {
data.instance.toggle_node(data.node);
};
treeChanged(e, data);
});
};


/*
* Update the plot if data exists, otherwise just open the tree.
* This is called when the `select_node` event is seen.
*/
function treeChanged(e, data) {
// If `metric_id` is defined, then we can query data
if (data.node.original.metric_id !== null) {
var expected = "/api/v1/data/" + data.node.original.metric_id;
// grab the plot data from the api
$.getJSON(expected)
.done(function(jsonData) {
makePlot(jsonData);

// This updates the URL to reflect which plot is shown.
var history_url = "/plot/" + data.node.original.metric_id;
window.history.pushState('page2', 'Title', history_url);
})
.fail(function(jqXHR, textStatus, errorThrown) {
console.log("Request failed: " + errorThrown);
});
} else {
// Otherwise just open/close the tree node.
data.instance.toggle_node(data.node);
};
}


/*
* Select a specific tree element.
* Called when both:
* (a) a metric_id is given in the URL and
* (b) the jsTree object has fully loaded.
*/
function selectNodeById(tree, metricId) {
if (typeof metricId === 'undefined') {
// We were given a metric ID, so let's select it in the jstree
tree.jstree('select_node', metricId);
}

}


/**
* Make the plotly Plot
*/
function makePlot(data) {
TESTER = document.getElementById('graph');

// Clear the plot before doing anything. Failure to do so results in each
// trace being appended.
Plotly.purge(TESTER);

// I think Plotly only accepts 1D arrays of data, so split things out.
var x = data.rows.map(function (obj) {return obj.timestamp});
var y = data.rows.map(function (obj) {return obj.value});
Expand Down
53 changes: 47 additions & 6 deletions src/trendlines/templates/trendlines/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,54 @@

{% block body %}

<div id="jstree-div">
</div>
<div id="content" class="container">
<div class="row">
<div class="col">
<h1><a href="{{ url_for('pages.index') }}">Trendlines</a></h1>
<h4>v{{ version }}</h4>
<div id="apiLinks">
<ul>
<li><a target="_blank" href="/api/">API Reference (Swagger)</a></li>
<li><a target="_blank" href="/api/redoc">API Reference (ReDoc)</a></li>
</ul>
</div>
</div>
</div>

<div class="row">
<div id="tree" class="col-sm-3 order-1 border border-primary" style="height: 150px; overflow-y: scroll;">
<!-- This div holds the left side: primarily the JS tree -->
<div id="jstree-div">
</div>

<script>
$(document).ready( function() {
var treeData = {{ tree_data | tojson | safe }};

// If we've been given a metric_id, then let's select that item in the tree.
// Doing so triggers the "changed" event.
var metricId = {{ metric_id | tojson | safe }};
populateTree(treeData, metricId);
});
</script>
</div>

<script>
var data = {{ data | tojson | safe }};
$(populateTree(data));
</script>
<div id="data" class="col-sm-9 order-2 border">
<!-- The right side: the plot and buttons -->
<div id="change-axis-buttons" class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-primary active">
<input id="x-axis-type-sequential" type="radio" name="x-axis-type" autocomplete="off" value="sequential" checked>Sequential
</label>
<label class="btn btn-primary">
<input id="x-axis-type-time" type="radio" name="x-axis-type" autocomplete="off" value="time">Time Series
</label>
</div>

<div id="graph" style="width:90%; height:500px;"></div>

</div>
</div>
</div>

{% endblock %}

Expand Down
18 changes: 5 additions & 13 deletions src/trendlines/templates/trendlines/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
<script type="text/javascript" charset="utf-8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript" charset="utf-8" src="https://cdn.plot.ly/plotly-1.43.0.min.js"></script>
<script type="text/javascript" charset="utf-8" src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/jstree.min.js"></script>
<script type="text/javascript" charset="utf-8" src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8" src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8" src="{{ url_for('static', filename='core.js') }}"></script>

<!-- CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/themes/default/style.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />

<title>Trendlines</title>

Expand All @@ -22,19 +25,8 @@
</header>

<main>
<h1><a href="{{ url_for('pages.index') }}">Trendlines</a></h1>
<h4>v{{ version }}</h4>
<div id="apiLinks">
<ul>
<li><a target="_blank" href="/api/">API Reference (Swagger)</a></li>
<li><a target="_blank" href="/api/redoc">API Reference (ReDoc)</a></li>
</ul>
</div>

<div id="content">
{% block body %}
{% endblock %}
</div>
{% block body %}
{% endblock %}
</main>

<footer>
Expand Down
25 changes: 0 additions & 25 deletions src/trendlines/templates/trendlines/plot.html

This file was deleted.

12 changes: 12 additions & 0 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def test_api_get_data_as_json(client, populated_db):
assert d[3]['value'] == 9


def test_api_get_data_by_id(client, populated_db):
rv = client.get("/api/v1/data/2")
assert rv.status_code == 200
assert rv.is_json
d = rv.get_json()
assert d['units'] == None
d = d['rows']
assert d[0]['n'] == 0
assert d[0]['value'] == 15
assert d[3]['value'] == 9


def test_api_get_data_as_json_metric_not_found(client):
rv = client.get("/api/v1/data/missing")
assert rv.status_code == 404
Expand Down

0 comments on commit 5621893

Please sign in to comment.