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.
This commit is contained in:
parent
9ef7497893
commit
7c7f52801d
File diff suppressed because one or more lines are too long
1
pydashie/assets/stylesheets/rickshaw.min.css
vendored
Normal file
1
pydashie/assets/stylesheets/rickshaw.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -61,7 +61,7 @@
|
||||
</li>
|
||||
|
||||
<li data-row="3" data-col="2" data-sizex="2" data-sizey="1">
|
||||
<div data-id="api_response" data-view="Graph" data-title="Api Response Times (ms)" data-moreinfo="out of 100 last queries"></div>
|
||||
<div data-id="api_response" data-view="Rickshawgraph" data-title="Api Response Times (ms)" data-moreinfo="out of 100 last queries" data-color-scheme="default" data-legend="true" data-unstack='true' data-renderer="area"></div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
@ -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()
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
// Sass declarations
|
||||
// ----------------------------------------------------------------------------
|
||||
$background-color: #12b0c5;
|
||||
$background-color: #118E9E;
|
||||
$value-color: #fff;
|
||||
|
||||
$title-color: rgba(255, 255, 255, 1);
|
||||
|
446
pydashie/widgets/rickshawgraph/rickshawgraph.coffee
Normal file
446
pydashie/widgets/rickshawgraph/rickshawgraph.coffee
Normal file
@ -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 = $("<div style='height: #{height}px;'></div>")
|
||||
$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 = $("<div style='width: #{width}px;'></div>")
|
||||
$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]
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
<h2 class="value" data-bind="current | prepend prefix"></h2>
|
||||
|
||||
<p class="more-info" data-bind="moreinfo"></p>
|
||||
<p class="more-info" data-bind="moreinfo"></p>
|
114
pydashie/widgets/rickshawgraph/rickshawgraph.scss
Normal file
114
pydashie/widgets/rickshawgraph/rickshawgraph.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user