From 7c7f52801d78de929bdc17695fa625768698593b Mon Sep 17 00:00:00 2001 From: adriant Date: Fri, 19 Dec 2014 14:48:01 +1300 Subject: [PATCH] reworked api response graph New graph widget, total reworking of response caching to avoid mutation of the cache while iterating. All timed responses now added to an event queue, which the APISamper processes and updates the graph with. --- pydashie/assets/stylesheets/application.css | 111 +++-- pydashie/assets/stylesheets/rickshaw.min.css | 1 + pydashie/main.py | 2 +- pydashie/openstack_app.py | 6 +- pydashie/openstack_samplers.py | 219 +++++---- pydashie/templates/main.html | 2 +- pydashie/widgets/graph/graph.coffee | 35 -- pydashie/widgets/graph/graph.scss | 70 --- pydashie/widgets/list/list.scss | 2 +- .../rickshawgraph/rickshawgraph.coffee | 446 ++++++++++++++++++ .../rickshawgraph.html} | 2 +- .../widgets/rickshawgraph/rickshawgraph.scss | 114 +++++ 12 files changed, 765 insertions(+), 245 deletions(-) create mode 100644 pydashie/assets/stylesheets/rickshaw.min.css delete mode 100644 pydashie/widgets/graph/graph.coffee delete mode 100644 pydashie/widgets/graph/graph.scss create mode 100644 pydashie/widgets/rickshawgraph/rickshawgraph.coffee rename pydashie/widgets/{graph/graph.html => rickshawgraph/rickshawgraph.html} (69%) create mode 100644 pydashie/widgets/rickshawgraph/rickshawgraph.scss diff --git a/pydashie/assets/stylesheets/application.css b/pydashie/assets/stylesheets/application.css index d1fda17..a7108eb 100644 --- a/pydashie/assets/stylesheets/application.css +++ b/pydashie/assets/stylesheets/application.css @@ -64,6 +64,75 @@ width: 100%; height: 100%; } +.widget-rickshawgraph { + background-color: #59615F; + position: relative; } + .widget-rickshawgraph .rickshaw_graph { + position: absolute; + left: 0px; + top: 0px; } + .widget-rickshawgraph h2 { + font-size: 16px; + white-space: pre; } + .widget-rickshawgraph svg { + position: absolute; + fill-opacity: 0.7; + left: 0px; + top: 0px; } + .widget-rickshawgraph .title, .widget-rickshawgraph .value { + position: relative; + z-index: 99; } + .widget-rickshawgraph .title { + color: white; } + .widget-rickshawgraph .more-info { + color: rgba(20, 20, 20, 0.8); + font-weight: 600; + font-size: 18px; + margin-top: 0; + margin-bottom: 20px; + z-index: 20; } + .widget-rickshawgraph .x_tick { + position: absolute; + bottom: 0; } + .widget-rickshawgraph .x_tick .title { + font-size: 20px; + color: black; + opacity: 0.5; + padding-bottom: 3px; } + .widget-rickshawgraph .y_ticks { + font-size: 20px; + fill: black; } + .widget-rickshawgraph .y_ticks text { + opacity: 0.5; } + .widget-rickshawgraph .domain { + display: none; } + .widget-rickshawgraph .rickshaw_legend { + position: absolute; + left: 0px; + bottom: 0px; + white-space: nowrap; + overflow-x: hidden; + font-size: 15px; + height: 20px; + padding: 5px 0px; + overflow-y: hidden; } + .widget-rickshawgraph .rickshaw_legend ul { + margin: 0; + padding: 0; + list-style-type: none; + text-align: center; } + .widget-rickshawgraph .rickshaw_legend ul li { + display: inline; } + .widget-rickshawgraph .rickshaw_legend .swatch { + display: inline-block; + width: 14px; + height: 14px; + margin-left: 5px; } + .widget-rickshawgraph .rickshaw_legend .label { + display: inline-block; + margin-left: 5px; + font-size: 17px; } + .widget-hotness { background-color: #000000; -webkit-transition: background-color 1s linear; @@ -166,43 +235,6 @@ .widget-usage-gauge .updated-at { color: rgba(255, 255, 255, 0.5); } -.widget-graph { - background-color: #1A773F; - position: relative; } - .widget-graph h2 { - font-size: 25px; - white-space: pre; } - .widget-graph svg { - position: absolute; - opacity: 0.6; - fill-opacity: 0.6; - left: 0px; - top: 0px; } - .widget-graph .title, .widget-graph .value { - position: relative; - z-index: 99; } - .widget-graph .title { - color: rgba(255, 255, 255, 0.8); } - .widget-graph .more-info { - color: white; - font-weight: 600; - font-size: 20px; - margin-top: 0; } - .widget-graph .x_tick { - position: absolute; - bottom: 0; } - .widget-graph .x_tick .title { - font-size: 20px; - color: rgba(0, 0, 0, 0.4); - opacity: 0.5; - padding-bottom: 3px; } - .widget-graph .y_ticks { - font-size: 20px; - fill: rgba(0, 0, 0, 0.4); - fill-opacity: 1; } - .widget-graph .domain { - display: none; } - .widget-comments { background-color: #eb9c3c; } .widget-comments .title { @@ -216,7 +248,7 @@ color: rgba(255, 255, 255, 0.7); } .widget-list { - background-color: #12b0c5; + background-color: #118E9E; vertical-align: top; } .widget-list .title { color: white; } @@ -866,4 +898,5 @@ li[class^="icon-"].icon-large:before, li[class*=" icon-"].icon-large:before { /* Uncomment this if you set helper : "clone" in draggable options */ /*.gridster .player { opacity:0; -}*/ \ No newline at end of file +}*/ +.rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} \ No newline at end of file diff --git a/pydashie/assets/stylesheets/rickshaw.min.css b/pydashie/assets/stylesheets/rickshaw.min.css new file mode 100644 index 0000000..d1b32d8 --- /dev/null +++ b/pydashie/assets/stylesheets/rickshaw.min.css @@ -0,0 +1 @@ +.rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} \ No newline at end of file diff --git a/pydashie/main.py b/pydashie/main.py index 52aa524..b3e88aa 100644 --- a/pydashie/main.py +++ b/pydashie/main.py @@ -59,7 +59,7 @@ def javascripts(): 'widgets/usage_gauge/usage_gauge.coffee', 'widgets/nagios/nagios.coffee', 'widgets/nagios_list/nagios_list.coffee', - 'widgets/graph/graph.coffee', + 'widgets/rickshawgraph/rickshawgraph.coffee' ] nizzle = True if not nizzle: diff --git a/pydashie/openstack_app.py b/pydashie/openstack_app.py index 000ecd8..11e11e7 100644 --- a/pydashie/openstack_app.py +++ b/pydashie/openstack_app.py @@ -10,14 +10,14 @@ from openstack_samplers import ( NagiosSampler, NagiosRegionSampler, ResourceSampler, - # ConvergenceSampler, + APISampler, ) def run(args, conf, app, xyzzy): client_cache = {} - response_cache = {'x': 0, 'items': collections.deque()} + response_cache = {'regions': {}, 'events': collections.deque()} samplers = [ CPUSampler(xyzzy, 60, conf['openstack'], client_cache, response_cache), @@ -33,7 +33,7 @@ def run(args, conf, app, xyzzy): NagiosRegionSampler(xyzzy, 15, conf['nagios']), ResourceSampler(xyzzy, 60, conf['openstack'], client_cache, response_cache), - # ConvergenceSampler(xyzzy, 1), + APISampler(xyzzy, 15, conf['openstack'], client_cache, response_cache), ] try: diff --git a/pydashie/openstack_samplers.py b/pydashie/openstack_samplers.py index 55d8a4d..af11934 100644 --- a/pydashie/openstack_samplers.py +++ b/pydashie/openstack_samplers.py @@ -1,6 +1,5 @@ import collections import datetime -import json from contextlib import contextmanager import nagios @@ -17,7 +16,7 @@ from neutronclient.v2_0 import client as neutronclient class BaseOpenstackSampler(DashieSampler): """docstring for ClassName""" def __init__(self, app, interval, conf=None, client_cache={}, - response_cache={'x': 0, 'items': collections.deque()}): + response_cache={}): self._os_clients = client_cache self._conf = conf self._response_cache = response_cache @@ -79,53 +78,14 @@ class BaseOpenstackSampler(DashieSampler): return self._os_clients[region][service] @contextmanager - def timed(self): + def timed(self, region): start = datetime.datetime.utcnow() yield end = datetime.datetime.utcnow() - self._api_response(int((end - start).total_seconds() * 1000)) + self._api_response(int((end - start).total_seconds() * 1000), region) - def _api_response(self, ms): - self._response_cache['items'].append({'x': self._response_cache['x'], - 'y': ms}) - self._response_cache['x'] += 1 - - # to stop the x value getting too high - if self._response_cache['x'] == 1000000: - # reset the x value, and adjust the items - self._response_cache['x'] = 0 - for time in self._response_cache['items']: - time['x'] = self._response_cache['x'] - self._response_cache['x'] += 1 - - if len(self._response_cache['items']) > 100: - self._response_cache['items'].popleft() - - stats = {'min': -1, 'max': -1, 'avg': -1} - - total = 0 - - for time in self._response_cache['items']: - total = total + time['y'] - if time['y'] > stats['max']: - stats['max'] = time['y'] - if stats['min'] == -1 or time['y'] < stats['min']: - stats['min'] = time['y'] - - stats['avg'] = int(total / len(self._response_cache['items'])) - - body = {} - body['displayedValue'] = ("min: %s max: %s avg: %s" % - (stats['min'], stats['max'], - stats['avg'])) - body['points'] = list(self._response_cache['items']) - body['id'] = 'api_response' - body['updatedAt'] = (datetime.datetime.now(). - strftime('%Y-%m-%d %H:%M:%S +0000')) - formatted_json = 'data: %s\n\n' % (json.dumps(body)) - self._app.last_events['api_response'] = formatted_json - for event_queue in self._app.events_queue.values(): - event_queue.put(formatted_json) + def _api_response(self, ms, region): + self._response_cache['events'].append({'region': region, 'ms': ms}) class CPUSampler(BaseOpenstackSampler): @@ -142,9 +102,9 @@ class CPUSampler(BaseOpenstackSampler): for region, allocation in self._conf['allocation'].iteritems(): nova = self._client('compute', region) - with self.timed(): + with self.timed(region): stats = nova.hypervisors.statistics() - with self.timed(): + with self.timed(region): hypervisors = nova.hypervisors.list() reserved = 0 @@ -180,9 +140,9 @@ class RAMSampler(BaseOpenstackSampler): for region, allocation in self._conf['allocation'].iteritems(): nova = self._client('compute', region) - with self.timed(): + with self.timed(region): stats = nova.hypervisors.statistics() - with self.timed(): + with self.timed(region): hypervisors = nova.hypervisors.list() reserved = 0 @@ -229,9 +189,9 @@ class IPSampler(BaseOpenstackSampler): neutron = self._client('network', region) - with self.timed(): + with self.timed(region): ips = neutron.list_floatingips() - with self.timed(): + with self.timed(region): routers = neutron.list_routers() net_gateways = 0 @@ -263,9 +223,9 @@ class RegionsCPUSampler(BaseOpenstackSampler): for region, allocation in self._conf['allocation'].iteritems(): nova = self._client('compute', region) - with self.timed(): + with self.timed(region): stats = nova.hypervisors.statistics() - with self.timed(): + with self.timed(region): hypervisors = nova.hypervisors.list() reserved = 0 @@ -296,9 +256,9 @@ class RegionsRAMSampler(BaseOpenstackSampler): for region, allocation in self._conf['allocation'].iteritems(): nova = self._client('compute', region) - with self.timed(): + with self.timed(region): stats = nova.hypervisors.statistics() - with self.timed(): + with self.timed(region): hypervisors = nova.hypervisors.list() reserved = 0 @@ -335,9 +295,9 @@ class RegionIPSampler(BaseOpenstackSampler): for region in self._conf['allocation'].keys(): neutron = self._client('network', region) - with self.timed(): + with self.timed(region): ips = neutron.list_floatingips() - with self.timed(): + with self.timed(region): routers = neutron.list_routers() net_gateways = 0 @@ -366,27 +326,30 @@ class NagiosSampler(BaseOpenstackSampler): def sample(self): - nagios.get_statusfiles(self._conf['services']) - servicestatus = nagios.parse_status(self._conf['services']) + try: + nagios.get_statusfiles(self._conf['services']) + servicestatus = nagios.parse_status(self._conf['services']) - criticals = 0 - warnings = 0 + criticals = 0 + warnings = 0 - for region in servicestatus: - criticals = criticals + servicestatus[region]['critical'] - warnings = warnings + servicestatus[region]['warning'] + for region in servicestatus: + criticals = criticals + servicestatus[region]['critical'] + warnings = warnings + servicestatus[region]['warning'] - status = 'green' + status = 'green' - if criticals > 0: - status = 'red' - elif warnings > 0: - status = 'yellow' + if criticals > 0: + status = 'red' + elif warnings > 0: + status = 'yellow' - s = {'criticals': criticals, - 'warnings': warnings, - 'status': status} - return s + s = {'criticals': criticals, + 'warnings': warnings, + 'status': status} + return s + except Exception, e: + print e class NagiosRegionSampler(BaseOpenstackSampler): @@ -394,29 +357,32 @@ class NagiosRegionSampler(BaseOpenstackSampler): return 'nagios_regions' def sample(self): - nagios.get_statusfiles(self._conf['services']) - servicestatus = nagios.parse_status(self._conf['services']) + try: + nagios.get_statusfiles(self._conf['services']) + servicestatus = nagios.parse_status(self._conf['services']) - criticals = [] - warnings = [] + criticals = [] + warnings = [] - for region in servicestatus: - criticals.append({'label': region, - 'value': servicestatus[region]['critical']}) - warnings.append({'label': region, - 'value': servicestatus[region]['warning']}) + for region in servicestatus: + criticals.append({'label': region, + 'value': servicestatus[region]['critical']}) + warnings.append({'label': region, + 'value': servicestatus[region]['warning']}) - # (adriant) the following is for easy testing: - # regions = ['region1', 'region2', 'region3'] + # (adriant) the following is for easy testing: + # regions = ['region1', 'region2', 'region3'] - # criticals = [] - # warnings = [] + # criticals = [] + # warnings = [] - # for region in regions: - # criticals.append({'label': region, 'value': random.randint(0, 5)}) - # warnings.append({'label': region, 'value': random.randint(0, 5)}) + # for region in regions: + # criticals.append({'label': region, 'value': random.randint(0, 5)}) + # warnings.append({'label': region, 'value': random.randint(0, 5)}) - return {'criticals': criticals, 'warnings': warnings} + return {'criticals': criticals, 'warnings': warnings} + except Exception, e: + print e class ResourceSampler(BaseOpenstackSampler): @@ -436,21 +402,21 @@ class ResourceSampler(BaseOpenstackSampler): nova = self._client('compute', region) # cinder = self._client('storage', region) - with self.timed(): + with self.timed(region): stats = nova.hypervisors.statistics() resources['instances'] = resources['instances'] + stats.running_vms - with self.timed(): + with self.timed(region): routers = neutron.list_routers() resources['routers'] = (resources['routers'] + len(routers['routers'])) - with self.timed(): + with self.timed(region): networks = neutron.list_networks() resources['networks'] = (resources['networks'] + len(networks['networks'])) - with self.timed(): + with self.timed(region): vpns = neutron.list_vpnservices() resources['vpns'] = (resources['vpns'] + len(vpns['vpnservices'])) @@ -464,3 +430,68 @@ class ResourceSampler(BaseOpenstackSampler): items.append({'label': key, 'value': value}) return {'items': items} + + +class APISampler(BaseOpenstackSampler): + def name(self): + return 'api_response' + + def sample(self): + while self._response_cache['events']: + self._process_event(self._response_cache['events'].popleft()) + + displayedValue = "" + regions = [] + + for region, cache in self._response_cache['regions'].iteritems(): + displayedValue += ("%s - (min: %s max: %s avg: %s)\n" % + (region, + cache['stats']['min'], + cache['stats']['max'], + cache['stats']['avg'])) + regions.append({'name': region, 'data': list(cache['items'])}) + + return {'displayedValue': displayedValue, 'series': regions} + + def _process_event(self, event): + + region_cache = self._response_cache['regions'].get(event['region']) + + if region_cache: + region_cache['items'].append({'x': region_cache['x'], + 'y': event['ms']}) + else: + region_cache = {} + region_cache['items'] = collections.deque() + region_cache['x'] = 0 + region_cache['items'].append({'x': region_cache['x'], + 'y': event['ms']}) + self._response_cache['regions'][event['region']] = region_cache + + region_cache['x'] += 1 + + # to stop the x value getting too high + if region_cache['x'] == 1000000: + # reset the x value, and adjust the items + region_cache['x'] = 0 + for time in region_cache['items']: + time['x'] = region_cache['x'] + region_cache['x'] += 1 + + if len(region_cache['items']) > 100: + region_cache['items'].popleft() + + stats = {'min': -1, 'max': -1, 'avg': -1} + + total = 0 + + for time in region_cache['items']: + total = total + time['y'] + if time['y'] > stats['max']: + stats['max'] = time['y'] + if stats['min'] == -1 or time['y'] < stats['min']: + stats['min'] = time['y'] + + stats['avg'] = int(total / len(region_cache['items'])) + + region_cache['stats'] = stats diff --git a/pydashie/templates/main.html b/pydashie/templates/main.html index 5c63f87..a705061 100644 --- a/pydashie/templates/main.html +++ b/pydashie/templates/main.html @@ -61,7 +61,7 @@
  • -
    +
  • diff --git a/pydashie/widgets/graph/graph.coffee b/pydashie/widgets/graph/graph.coffee deleted file mode 100644 index b8184b9..0000000 --- a/pydashie/widgets/graph/graph.coffee +++ /dev/null @@ -1,35 +0,0 @@ -class Dashing.Graph extends Dashing.Widget - - @accessor 'current', -> - return @get('displayedValue') if @get('displayedValue') - points = @get('points') - if points - points[points.length - 1].y - - ready: -> - container = $(@node).parent() - # Gross hacks. Let's fix this. - width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1) - height = (Dashing.widget_base_dimensions[1] * container.data("sizey")) - @graph = new Rickshaw.Graph( - element: @node - width: width - height: height - series: [ - { - color: "#fff", - data: [{x:0, y:0}] - } - ] - ) - - @graph.series[0].data = @get('points') if @get('points') - - # x_axis = new Rickshaw.Graph.Axis.Time(graph: @graph) - y_axis = new Rickshaw.Graph.Axis.Y(graph: @graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT) - @graph.render() - - onData: (data) -> - if @graph - @graph.series[0].data = data.points - @graph.render() diff --git a/pydashie/widgets/graph/graph.scss b/pydashie/widgets/graph/graph.scss deleted file mode 100644 index 28add8b..0000000 --- a/pydashie/widgets/graph/graph.scss +++ /dev/null @@ -1,70 +0,0 @@ -// ---------------------------------------------------------------------------- -// Sass declarations -// ---------------------------------------------------------------------------- -$background-color: #1A773F; - -$title-color: rgba(255, 255, 255, 0.8); -$moreinfo-color: rgba(255, 255, 255, 1); -$tick-color: rgba(0, 0, 0, 0.4); - - -// ---------------------------------------------------------------------------- -// Widget-graph styles -// ---------------------------------------------------------------------------- -.widget-graph { - - background-color: $background-color; - position: relative; - - h2{ - font-size: 25px; - white-space: pre; - } - - - svg { - position: absolute; - opacity: 0.6; - fill-opacity: 0.6; - left: 0px; - top: 0px; - } - - .title, .value { - position: relative; - z-index: 99; - } - - .title { - color: $title-color; - } - - .more-info { - color: $moreinfo-color; - font-weight: 600; - font-size: 20px; - margin-top: 0; - } - - .x_tick { - position: absolute; - bottom: 0; - .title { - font-size: 20px; - color: $tick-color; - opacity: 0.5; - padding-bottom: 3px; - } - } - - .y_ticks { - font-size: 20px; - fill: $tick-color; - fill-opacity: 1; - } - - .domain { - display: none; - } - -} \ No newline at end of file diff --git a/pydashie/widgets/list/list.scss b/pydashie/widgets/list/list.scss index 9685347..1342099 100644 --- a/pydashie/widgets/list/list.scss +++ b/pydashie/widgets/list/list.scss @@ -1,7 +1,7 @@ // ---------------------------------------------------------------------------- // Sass declarations // ---------------------------------------------------------------------------- -$background-color: #12b0c5; +$background-color: #118E9E; $value-color: #fff; $title-color: rgba(255, 255, 255, 1); diff --git a/pydashie/widgets/rickshawgraph/rickshawgraph.coffee b/pydashie/widgets/rickshawgraph/rickshawgraph.coffee new file mode 100644 index 0000000..e2f23cf --- /dev/null +++ b/pydashie/widgets/rickshawgraph/rickshawgraph.coffee @@ -0,0 +1,446 @@ +# Rickshawgraph v0.1.0 + +class Dashing.Rickshawgraph extends Dashing.Widget + + DIVISORS = [ + {number: 100000000000000000000000, label: 'Y'}, + {number: 100000000000000000000, label: 'Z'}, + {number: 100000000000000000, label: 'E'}, + {number: 1000000000000000, label: 'P'}, + {number: 1000000000000, label: 'T'}, + {number: 1000000000, label: 'G'}, + {number: 1000000, label: 'M'}, + {number: 1000, label: 'K'} + ] + + # Take a long number like "2356352" and turn it into "2.4M" + formatNumber = (number) -> + for divisior in DIVISORS + if number > divisior.number + number = "#{Math.round(number / (divisior.number/10))/10}#{divisior.label}" + break + + return number + + getRenderer: () -> return @get('renderer') or @get('graphtype') or 'area' + + # Retrieve the `current` value of the graph. + @accessor 'current', -> + answer = null + + # Return the value supplied if there is one. + if @get('displayedValue') != null and @get('displayedValue') != undefined + answer = @get('displayedValue') + + if answer == null + # Compute a value to return based on the summaryMethod + series = @_parseData {points: @get('points'), series: @get('series')} + if !(series?.length > 0) + # No data in series + answer = '' + + else + switch @get('summaryMethod') + when "sum" + answer = 0 + answer += (point?.y or 0) for point in s.data for s in series + + when "sumLast" + answer = 0 + answer += s.data[s.data.length - 1].y or 0 for s in series + + when "highest" + answer = 0 + if @get('unstack') or (@getRenderer() is "line") + answer = Math.max(answer, (point?.y or 0)) for point in s.data for s in series + else + # Compute the sum of values at each point along the graph + for index in [0...series[0].data.length] + value = 0 + for s in series + value += s.data[index]?.y or 0 + answer = Math.max(answer, value) + + when "none" + answer = '' + + else + # Otherwise if there's only one series, pick the most recent value from the series. + if series.length == 1 and series[0].data?.length > 0 + data = series[0].data + answer = data[data.length - 1].y + else + # Otherwise just return nothing. + answer = '' + + answer = formatNumber answer + + return answer + + + ready: -> + @assignedColors = @get('colors').split(':') if @get('colors') + @strokeColors = @get('strokeColors').split(':') if @get('strokeColors') + + @graph = @_createGraph() + @graph.render() + + clear: -> + # Remove the old graph/legend if there is one. + $node = $(@node) + $node.find('.rickshaw_graph').remove() + if @$legendDiv + @$legendDiv.remove() + @$legendDiv = null + + # Handle new data from Dashing. + onData: (data) -> + series = @_parseData data + + if @graph + # Remove the existing graph if the number of series has changed or any names have changed. + needClear = false + needClear |= (series.length != @graph.series.length) + if @get("legend") then for subseries, index in series + needClear |= @graph.series[index]?.name != series[index]?.name + + if needClear then @graph = @_createGraph() + + # Copy over the new graph data + for subseries, index in series + @graph.series[index] = subseries + + @graph.render() + + # Create a new Rickshaw graph. + _createGraph: -> + $node = $(@node) + $container = $node.parent() + + @clear() + + # Gross hacks. Let's fix this. + width = (Dashing.widget_base_dimensions[0] * $container.data("sizex")) + Dashing.widget_margins[0] * 2 * ($container.data("sizex") - 1) + height = (Dashing.widget_base_dimensions[1] * $container.data("sizey")) + + if @get("legend") + # Shave 20px off the bottom of the graph for the legend + height -= 30 + + $graph = $("
    ") + $node.append $graph + series = @_parseData {points: @get('points'), series: @get('series')} + + graphOptions = { + element: $graph.get(0), + renderer: @getRenderer(), + width: width, + height: height, + series: series + } + + if !!@get('stroke') then graphOptions.stroke = true + if @get('min') != null then graphOptions.max = @get('min') + if @get('max') != null then graphOptions.max = @get('max') + + try + graph = new Rickshaw.Graph graphOptions + catch err + if err.toString() is "x and y properties of points should be numbers instead of number and object" + # This will happen with older versions of Rickshaw that don't support nulls in the data set. + nullsFound = false + for s in series + for point in s.data + if point.y is null + nullsFound = true + point.y = 0 + + if nullsFound + # Try to create the graph again now that we've patched up the data. + graph = new Rickshaw.Graph graphOptions + if !@rickshawVersionWarning + console.log "#{@get 'id'} - Nulls were found in your data, but Rickshaw didn't like" + + " them. Consider upgrading your rickshaw to 1.4.3 or higher." + @rickshawVersionWarning = true + else + # No nulls were found - this is some other problem, so just re-throw the exception. + throw err + + graph.renderer.unstack = !!@get('unstack') + + xAxisOptions = { + graph: graph + } + if Rickshaw.Fixtures.Time.Local + xAxisOptions.timeFixture = new Rickshaw.Fixtures.Time.Local() + + # x_axis = new Rickshaw.Graph.Axis.Time xAxisOptions + y_axis = new Rickshaw.Graph.Axis.Y(graph: graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT) + + if @get("legend") + # Add a legend + @$legendDiv = $("
    ") + $node.append(@$legendDiv) + legend = new Rickshaw.Graph.Legend { + graph: graph + element: @$legendDiv.get(0) + } + + return graph + + # Parse a {series, points} object with new data from Dashing. + # + _parseData: (data) -> + series = [] + + # Figure out what kind of data we've been passed + if data.series + dataSeries = if isString(data.series) then JSON.parse data.series else data.series + for subseries, index in dataSeries + try + series.push @_parseSeries subseries + catch err + console.log "Error while parsing series: #{err}" + + else if data.points + points = data.points + if isString(points) then points = JSON.parse points + + if points[0]? and !points[0].x? + # Not already in Rickshaw format; assume graphite data + points = graphiteDataToRickshaw(points) + + series.push {data: points} + + if series.length is 0 + # No data - create a dummy series to keep Rickshaw happy + series.push {data: [{x:0, y:0}]} + + @_updateColors(series) + + # Fix any missing data in the series. + if Rickshaw.Series.fill then Rickshaw.Series.fill(series, null) + + return series + + # Parse a series of data from an array passed to `_parseData()`. + # This accepts both Graphite and Rickshaw style data sets. + _parseSeries: (series) -> + if series?.datapoints? + # This is a Graphite series + answer = { + name: series.target + data: graphiteDataToRickshaw series.datapoints + color: series.color + stroke: series.stroke + } + else if series?.data? + # Rickshaw data. Need to clone, otherwise we could end up with multiple graphs sharing + # the same data, and Rickshaw really doesn't like that. + answer = { + name: series.name + data: series.data + color: series.color + stroke: series.stroke + } + else if !series + throw new Error("No data received for #{@get 'id'}") + else + throw new Error("Unknown data for #{@get 'id'}. series: #{series}") + + answer.data.sort (a,b) -> a.x - b.x + + return answer + + # Update the color assignments for a series. This will assign colors to any data that + # doesn't have a color already. + _updateColors: (series) -> + # If no colors were provided, or of there aren't enough colors, then generate a set of + # colors to use. + if !@defaultColors or @defaultColors?.length != series.length + @defaultColors = computeDefaultColors @, @node, series + + for subseries, index in series + # Preferentially pick supplied colors instead of defaults, but don't overwrite a color + # if one was supplied with the data. + subseries.color ?= @assignedColors?[index] or @defaultColors[index] + subseries.stroke ?= @strokeColors?[index] or "#000" + + # Convert a collection of Graphite data points into data that Rickshaw will understand. + graphiteDataToRickshaw = (datapoints) -> + answer = [] + for datapoint in datapoints + # Need to convert potential nulls from Graphite into a real number for Rickshaw. + answer.push {x: datapoint[1], y: (datapoint[0] or 0)} + answer + + # Compute a pleasing set of default colors. This works by starting with the background color, + # and picking colors of intermediate luminance between the background and white (or the + # background and black, for light colored backgrounds.) We use the brightest color for the + # first series, because then multiple series will appear to blend in to the background. + computeDefaultColors = (self, node, series) -> + defaultColors = [] + + # Use a neutral color if we can't get the background-color for some reason. + backgroundColor = parseColor($(node).css('background-color')) or [50, 50, 50, 1.0] + hsl = rgbToHsl backgroundColor + + alpha = if self.get('defaultAlpha')? then self.get('defaultAlpha') else 1 + + if self.get('colorScheme') in ['rainbow', 'near-rainbow'] + saturation = (interpolate hsl[1], 1.0, 3)[1] + luminance = if (hsl[2] < 0.6) then 0.7 else 0.3 + + hueOffset = 0 + if self.get('colorScheme') is 'rainbow' + # Note the first and last values in `hues` will both have the same hue as the background, + # hence the + 2. + hues = interpolate hsl[0], hsl[0] + 1, (series.length + 2) + hueOffset = 1 + else + hues = interpolate hsl[0] - 0.25, hsl[0] + 0.25, series.length + for hue, index in hues + if hue > 1 then hues[index] -= 1 + if hue < 0 then hues[index] += 1 + + for index in [0...series.length] + defaultColors[index] = rgbToColor hslToRgb([hues[index + hueOffset], saturation, luminance, alpha]) + + else + hue = if self.get('colorScheme') is "compliment" then hsl[0] + 0.5 else hsl[0] + if hsl[0] > 1 then hsl[0] -= 1 + + saturation = hsl[1] + saturationSource = if (saturation < 0.6) then 0.7 else 0.3 + saturations = interpolate saturationSource, saturation, (series.length + 1) + + luminance = hsl[2] + luminanceSource = if (luminance < 0.6) then 0.9 else 0.1 + luminances = interpolate luminanceSource, luminance, (series.length + 1) + + for index in [0...series.length] + defaultColors[index] = rgbToColor hslToRgb([hue, saturations[index], luminances[index], alpha]) + + return defaultColors + + + +# Helper functions +# ================ +isString = (obj) -> + return toString.call(obj) is "[object String]" + +# Parse a `rgb(x,y,z)` or `rgba(x,y,z,a)` string. +parseRgbaColor = (colorString) -> + match = /^rgb\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString) + if match + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), 1.0] + + match = /^rgba\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString) + if match + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4])] + + return null + +# Parse a color string as RGBA +parseColor = (colorString) -> + answer = null + + # Try to use the browser to parse the color for us. + div = document.createElement('div') + div.style.color = colorString + if div.style.color + answer = parseRgbaColor div.style.color + + if !answer + match = /^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/.exec(colorString) + if match then answer = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16), 1.0] + + if !answer + match = /^#([\da-fA-F])([\da-fA-F])([\da-fA-F])/.exec(colorString) + if match then answer = [parseInt(match[1], 16) * 0x11, parseInt(match[2], 16) * 0x11, parseInt(match[3], 16) * 0x11, 1.0] + + if !answer then answer = parseRgbaColor colorString + + return answer + +# Convert an RGB or RGBA color to a CSS color. +rgbToColor = (rgb) -> + if (!3 of rgb) or (rgb[3] == 1.0) + return "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})" + else + return "rgba(#{rgb[0]},#{rgb[1]},#{rgb[2]},#{rgb[3]})" + +# Returns an array of size `steps`, where the first value is `source`, the last value is `dest`, +# and the intervening values are interpolated. If steps < 2, then returns `[dest]`. +# +interpolate = (source, dest, steps) -> + if steps < 2 + answer =[dest] + else + stepSize = (dest - source) / (steps - 1) + answer = (num for num in [source..dest] by stepSize) + # Rounding errors can cause us to drop the last value + if answer.length < steps then answer.push dest + + return answer + +# Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c +# +# Converts an RGBA color value to HSLA. Conversion formula +# adapted from http://en.wikipedia.org/wiki/HSL_color_space. +# Assumes r, g, and b are contained in the set [0, 255] and +# a in [0, 1]. Returns h, s, l, a in the set [0, 1]. +# +# Returns the HSLA representation as an array. +rgbToHsl = (rgba) -> + [r,g,b,a] = rgba + r /= 255 + g /= 255 + b /= 255 + max = Math.max(r, g, b) + min = Math.min(r, g, b) + l = (max + min) / 2 + + if max == min + h = s = 0 # achromatic + else + d = max - min + s = if l > 0.5 then d / (2 - max - min) else d / (max + min) + switch max + when r then h = (g - b) / d + (g < b ? 6 : 0) + when g then h = (b - r) / d + 2 + when b then h = (r - g) / d + 4 + h /= 6; + + return [h, s, l, a] + +# Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c +# +# Converts an HSLA color value to RGBA. Conversion formula +# adapted from http://en.wikipedia.org/wiki/HSL_color_space. +# Assumes h, s, l, and a are contained in the set [0, 1] and +# returns r, g, and b in the set [0, 255] and a in [0, 1]. +# +# Retunrs the RGBA representation as an array. +hslToRgb = (hsla) -> + [h,s,l,a] = hsla + if s is 0 + r = g = b = l # achromatic + else + hue2rgb = (p, q, t) -> + if(t < 0) then t += 1 + if(t > 1) then t -= 1 + if(t < 1/6) then return p + (q - p) * 6 * t + if(t < 1/2) then return q + if(t < 2/3) then return p + (q - p) * (2/3 - t) * 6 + return p + + q = if l < 0.5 then l * (1 + s) else l + s - l * s + p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3) + g = hue2rgb(p, q, h) + b = hue2rgb(p, q, h - 1/3) + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a] + diff --git a/pydashie/widgets/graph/graph.html b/pydashie/widgets/rickshawgraph/rickshawgraph.html similarity index 69% rename from pydashie/widgets/graph/graph.html rename to pydashie/widgets/rickshawgraph/rickshawgraph.html index ad14f8d..da62631 100644 --- a/pydashie/widgets/graph/graph.html +++ b/pydashie/widgets/rickshawgraph/rickshawgraph.html @@ -2,4 +2,4 @@

    -

    \ No newline at end of file +

    diff --git a/pydashie/widgets/rickshawgraph/rickshawgraph.scss b/pydashie/widgets/rickshawgraph/rickshawgraph.scss new file mode 100644 index 0000000..c80f517 --- /dev/null +++ b/pydashie/widgets/rickshawgraph/rickshawgraph.scss @@ -0,0 +1,114 @@ +// ---------------------------------------------------------------------------- +// Sass declarations +// ---------------------------------------------------------------------------- +$background-color: #59615F; + +$title-color: rgba(255, 255, 255, 1); +$moreinfo-color: rgba(20, 20, 20, 0.8); +$tick-color: rgba(0, 0, 0, 1); + + +// ---------------------------------------------------------------------------- +// Widget-graph styles +// ---------------------------------------------------------------------------- +.widget-rickshawgraph { + + background-color: $background-color; + position: relative; + + .rickshaw_graph { + position: absolute; + left: 0px; + top: 0px; + } + + h2{ + font-size: 16px; + white-space: pre; + } + + svg { + position: absolute; + fill-opacity: 0.7; + left: 0px; + top: 0px; + } + + .title, .value { + position: relative; + z-index: 99; + } + + .title { + color: $title-color; + } + + .more-info { + color: $moreinfo-color; + font-weight: 600; + font-size: 18px; + margin-top: 0; + margin-bottom: 20px; + z-index: 20; + } + + .x_tick { + position: absolute; + bottom: 0; + .title { + font-size: 20px; + color: $tick-color; + opacity: 0.5; + padding-bottom: 3px; + } + } + + .y_ticks { + font-size: 20px; + fill: $tick-color; + text { + opacity: 0.5; + } + } + + .domain { + display: none; + } + + .rickshaw_legend { + position: absolute; + left: 0px; + bottom: 0px; + white-space: nowrap; + overflow-x: hidden; + font-size: 15px; + height: 20px; + padding: 5px 0px; + overflow-y: hidden; + + ul { + margin: 0; + padding: 0; + list-style-type: none; + text-align: center; + } + + ul li { + display: inline; + } + + .swatch { + display: inline-block; + width: 14px; + height: 14px; + margin-left: 5px; + } + + .label { + display: inline-block; + margin-left: 5px; + font-size: 17px; + } + } + +}