Skip to content

Commit

Permalink
Frontend generated node charts with graphite backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Rüttgers committed Jan 14, 2016
1 parent e80e8b2 commit d12ab04
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 5 deletions.
3 changes: 2 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ require.config({
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
"rbush": "../bower_components/rbush/rbush",
"helper": "../helper",
"jshashes": "../bower_components/jshashes/hashes"
"jshashes": "../bower_components/jshashes/hashes",
"c3": "../bower_components/c3/c3.min"
},
shim: {
"leaflet.label": ["leaflet"],
Expand Down
8 changes: 6 additions & 2 deletions bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
"virtual-dom": "~2.0.1",
"leaflet-providers": "~1.0.27",
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
"jshashes": "~1.0.5"
"jshashes": "~1.0.5",
"c3": "~0.4.10"
},
"authors": [
"Nils Schneider <nils@nilsschneider.net>"
],
"license": "GPL3",
"private": true
"private": true,
"resolutions": {
"d3": "~3.5.5"
}
}
16 changes: 16 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,21 @@
{
"name": "Stamen.TonerLite"
}
],
"nodeCharts": [
{
"name": "Statistik",
"data": {
"url": "http://137.226.33.62:8002/render",
"parameters": [
"format=json",
"from=-###ZOOM_FROM###",
"target=alias(summarize(freifunk.nodes-legacy.###NODE_ID###.clientcount,\"###ZOOM_INTERVAL###\",\"max\"),\"clients\")",
"target=alias(summarize(freifunk.nodes-legacy.###NODE_ID###.loadavg,\"###ZOOM_INTERVAL###\",\"avg\"),\"load\")",
"target=alias(summarize(freifunk.nodes-legacy.###NODE_ID###.uptime,\"###ZOOM_INTERVAL###\",\"last\"),\"uptime\")"
]
},
"quirks": [ "id_to_mac" ]
}
]
}
1 change: 1 addition & 0 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<link rel="stylesheet" href="css/ionicons.min.css">
<link rel="stylesheet" href="roboto-slab-fontface.css">
<link rel="stylesheet" href="roboto-fontface.css">
<link rel="stylesheet" href="c3.min.css">
<link rel="stylesheet" href="style.css">
<script src="vendor/es6-shim/es6-shim.min.js"></script>
<script src="app.js"></script>
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
<link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
<link rel="stylesheet" href="style.css">
<script src="bower_components/es6-shim/es6-shim.min.js"></script>
<script src="bower_components/requirejs/require.js" data-main="app"></script>
Expand Down
258 changes: 258 additions & 0 deletions lib/infobox/charts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
define(["c3", "d3"], function (c3, d3) {

var charts = function (node, config) {
this.node = node

this.chartConfig = config
this.chart = null

this.zoomConfig = {
"levels": [
{"label": "8h", "from": "8h", "interval": "15min"},
{"label": "24h", "from": "26h", "interval": "1h"},
{"label": "1m", "from": "1mon", "interval": "1d"},
{"label": "1y", "from": "1y", "interval": "1mon"}
]
}
this.zoomLevel = 0

this.c3Config = {
"size": {
"height": 240
},
padding: {
bottom: 30
},
"legend": {
"item": {
"onclick": function (id) {
this.api.hide()
this.api.show(id)
}
}
},
"data": {
"keys": {
"x": "time",
"value": ["clients", "load", "uptime"]
},
"colors": {
"clients": "#1566A9",
"load": "#1566A9",
"uptime": "#1566A9"
},
"names": {
"clients": "Clients",
"load": "Load",
"uptime": "Uptime"
},
"hide": ["load", "uptime"]
},
"tooltip": {
"format": {
"value": this.c3FormatToolTip.bind(this)
}
},
"axis": {
"x": {
"type": "timeseries",
"tick": {
"format": this.c3FormatXAxis.bind(this),
"rotate": -45
}
},
"y": {
"min": 0,
"padding": {
"bottom": 0
}
}
}
}

this.cache = []

this.init()
}

charts.prototype = {

init: function () {
// Workaround for endless loop bug
if (this.c3Config.axis.x.tick && this.c3Config.axis.x.tick.format && typeof this.c3Config.axis.x.tick.format === "function") {
if (this.c3Config.axis.x.tick.format && !this.c3Config.axis.x.tick._format)
this.c3Config.axis.x.tick._format = this.c3Config.axis.x.tick.format
this.c3Config.axis.x.tick.format = function (val) {
return this.c3Config.axis.x.tick._format(val)
}.bind(this)
}
},

render: function () {
var div = document.createElement("div")
div.classList.add("chart")
var h4 = document.createElement("h4")
h4.textContent = this.chartConfig.name
div.appendChild(h4)


// Render chart
this.load(function (data) {
div.appendChild(this.renderChart(data))

// Render zoom controls
if (this.zoomConfig.levels.length > 0)
div.appendChild(this.renderZoomControls())

}.bind(this))

return div
},

renderChart: function (data) {
this.c3Config.data.json = data
this.chart = c3.generate(this.c3Config)
return this.chart.element
},

updateChart: function (data) {
this.c3Config.data.json = data
this.chart.load(this.c3Config.data)
},

renderZoomControls: function () {
// Draw zoom controls
var zoomDiv = document.createElement("div")
zoomDiv.classList.add("zoom-buttons")

var zoomButtons = []
this.zoomConfig.levels.forEach(function (v, level) {
var btn = document.createElement("button")
btn.classList.add("zoom-button")
btn.setAttribute("data-zoom-level", level)

if (level === this.zoomLevel)
btn.classList.add("active")

btn.onclick = function () {
if (level !== this.zoomLevel) {
zoomButtons.forEach(function (v, k) {
if (level !== k)
v.classList.remove("active")
else
v.classList.add("active")
})
this.setZoomLevel(level)
}
}.bind(this)
btn.textContent = v.label
zoomButtons[level] = btn
zoomDiv.appendChild(btn)
}.bind(this))
return zoomDiv
},

setZoomLevel: function (level) {
if (level !== this.zoomLevel) {
this.zoomLevel = level
this.load(this.updateChart.bind(this))
}
},

load: function (callback) {
if (this.cache[this.zoomLevel])
callback(this.cache[this.zoomLevel])
else {
var url = this.chartConfig.data.url +
"?" +
this.chartConfig.data.parameters.join("&")

var zoomConfig = this.zoomConfig.levels[this.zoomLevel]

var id = this.node.nodeinfo.node_id

if (this.chartConfig.quirks
&& Array.isArray(this.chartConfig.quirks)
&& this.chartConfig.quirks.indexOf("id_to_mac") >= 0) {

// Quirk for legacy graphite data of Freifunk Aachen (data is stored by the node's mac address)
var regex = /^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i
var match = regex.exec(id)
if (match)
id = match[1] + ":" + match[2] + ":" + match[3] + ":" + match[4] + ":" + match[5] + ":" + match[6]
}

// Using split as workaround for replacing all occurrences (to avoid regexps)
url = url.split("###NODE_ID###").join(id)
url = url.split("###ZOOM_FROM###").join(zoomConfig.from)
url = url.split("###ZOOM_INTERVAL###").join(zoomConfig.interval)

// In case we will have multiple urls in the future
Promise.all([url].map(getJSON)).then(function (data) {
this.cache[this.zoomLevel] = this.parse(data)
callback(this.cache[this.zoomLevel])
}.bind(this))
}
},

parse: function (results) {
var data = []
results.forEach(function (d) {
if (d[0] && d[0].target && d[0].datapoints)
d[0].datapoints.forEach(function (dp, dpk) {
var tmp = {"time": new Date(dp[1] * 1000)}
for (var i = 0; i < d.length; i++) {
var target = d[i].target
var v = (d[i].datapoints[dpk] ? d[i].datapoints[dpk][0] : 0)
tmp[target] = this.formatValue(target, v)
}
data.push(tmp)
}.bind(this))
}.bind(this))
return data
},

c3FormatToolTip: function (d, ratio, id) {
switch (id) {
case "uptime":
return d.toFixed(1) + " Tage"
default:
return d
}
},

c3FormatXAxis: function (d) {
var pad = function (number, pad) {
var N = Math.pow(10, pad)
return number < N ? ("" + (N + number)).slice(1) : "" + number
}
switch (this.zoomLevel) {
case 0: // 8h
case 1: // 24h
return pad(d.getHours(), 2) + ":" + pad(d.getMinutes(), 2)
case 2: // 1m
return pad(d.getDate(), 2) + "." + pad(d.getMonth() + 1, 2)
case 3: // 1y
return ["Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][d.getMonth()]
default:
break
}
},

formatValue: function (id, value) {
switch (id) {
case "load":
return (d3.format(".2r")(value))
case "clients":
return (Math.ceil(value))
case "uptime":
return (value / 86400)
}
return value
}

}


return charts
})
8 changes: 6 additions & 2 deletions lib/infobox/node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
define(["moment", "numeral", "tablesort", "tablesort.numeric"],
function (moment, numeral, Tablesort) {
define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"],
function (moment, numeral, Tablesort, Charts) {
function showGeoURI(d) {
function showLatitude(d) {
var suffix = Math.sign(d) > -1 ? "' N" : "' S"
Expand Down Expand Up @@ -200,6 +200,10 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],

el.appendChild(attributes)

if (!d.flags.gateway && config.nodeCharts)
config.nodeCharts.forEach( function (config) {
el.appendChild((new Charts(d, config)).render())
})

if (config.nodeInfos)
config.nodeInfos.forEach( function (nodeInfo) {
Expand Down
20 changes: 20 additions & 0 deletions scss/_chart.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.infobox .chart {
position: relative;
& > .c3 {
margin-right: 20px;
}

.zoom-buttons {
position: absolute;
top: 0px;
right: 20px;

button {
font-size: 10pt;
width: 3em;
height: 3em;
border-radius: 1.5em;
margin-left: 6px;
}
}
}
1 change: 1 addition & 0 deletions scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ $buttondistance: 12pt;
@import '_map';
@import '_forcegraph';
@import '_legend';
@import '_chart';

.content {
position: fixed;
Expand Down
6 changes: 6 additions & 0 deletions tasks/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ module.exports = function(grunt) {
out: "build/app.js"
}
}
},
c3: {
src: ["c3.min.css"],
expand: true,
dest: "build/",
cwd: "bower_components/c3/"
}
})

Expand Down

0 comments on commit d12ab04

Please sign in to comment.