Changed tweets, added eca

This commit is contained in:
2022-10-25 12:15:41 +02:00
parent 7597d7648a
commit 9860bee497
71 changed files with 7476 additions and 2036 deletions

BIN
.DS_Store vendored Normal file
View File

Binary file not shown.

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
ECA - Event Condition Action
=================================
ECA was developed as an educational tool aimed at a diverse CS student
population with programming experience ranging from "very little" to "very
much": it allows students to quickly develop a prototype real-time Twitter
Dashboard that really looks cool. And it is open to more advanced programming
to challenge students who have more experience.
The basis of ECA is a rule system that reacts to externally generated events.
The architecture favours simplicity over robustness. Multithreading is used in
favour of more suitable parallelism options such as greenlets to keep
dependencies to a minimum for ease of deployment.
Documentation can be found in the Wiki of this github site.
This program is not intended for production use. It may contain security issues
not tolerable outside of a controlled environment.
ECA requires Python 3.2 or higher.

21
TODO.txt Normal file
View File

@@ -0,0 +1,21 @@
[x] Refactor handlers in neca.py to eca/http.py
[x] Add session manager to lazy-create new Contexts
[x] Context switching through session manager
[x] Add SSE handler to allow event-based pushes
[x] Add pubsub for SSE handlers to allow emitting from within ECA context
[x] Rework static file handling to not be the exception to normal operations
[x] Add basic Javascript handling of events
[x] Add a way to configure twitter (event generation) input thread
[ ] Add control to event generation thread through HTTP interface
[x] Document all core modules
[ ] Add sample component of a graph/twitter list
[ ] Harden system against programmer error (i.e. sanity check all rules-module data flow)
[x] Add a delayed event scheduler (for 'fire in 2 seconds' things)
[x] locatie tweets http://library.ewi.utwente.nl/ecadata/batatweets.txt
[x] Clean up samples and rename leading sample to template.py + template dir
[x] Button Block
[ ] Session block
[x] Full speed tweet time_factor
[x] Docs in graph.js
[x] emit() in non-server context should doe something useful
[x] Rework rules set to non-global object

56
demos/advancedcontexts.py Normal file
View File

@@ -0,0 +1,56 @@
from eca import *
import random
# declare two separate rule sets
consumer = Rules()
producer = Rules()
# default init
@event('init')
def bootstrap(c, e):
# start a few contexts for the generation of stock quotes
# (The simple workload here can easily be done in a single context, but we
# do this in separate contexts to give an example of the feature.)
spawn_context({'symbol':'GOOG', 'start':500.0, 'delay':1.2}, rules=producer, daemon=True)
spawn_context({'symbol':'AAPL', 'start':99.0, 'delay':0.9}, rules=producer, daemon=True)
spawn_context(rules=consumer, daemon=True)
@event('end-of-input')
def done(c,e):
# terminate if the input is closed
shutdown()
# producer rules
@producer.event('init')
def start_work(c, e):
c.symbol = e.data['symbol']
c.delay = e.data['delay']
fire('sample', {
'previous': e.data['start']
})
@producer.event('sample')
def work(c, e):
current = e.data['previous'] + random.uniform(-0.5, 0.5)
fire_global('quote', {
'symbol': c.symbol,
'value': current
})
fire('sample', {
'previous': current
}, delay=c.delay)
# consumer rules
@consumer.event('quote')
def show_quote(c, e):
print("Quote for {symbol}: {value}".format(**e.data))

47
demos/average.py Normal file
View File

@@ -0,0 +1,47 @@
from eca import *
@event('main')
def setup(ctx, e):
"""
Initialise the context with an accumulator value, and inform
the user about the fact that we process input.
"""
print("Enter a number per line and end with EOF:")
print("(EOF is ctrl+d under linux and MacOSes, ctrl+z followed by return under Windows)")
ctx.accumulator = 0
ctx.count = 0
@event('line')
def line(ctx, e):
"""
Tries to parse the input line as a number and add it to the accumulator.
"""
try:
value = float(e.data) if '.' in e.data else int(e.data)
ctx.accumulator += value
ctx.count += 1
print("sum = " + str(ctx.accumulator))
except ValueError:
print("'{}' is not a number.".format(e.data))
@event('end-of-input')
@condition(lambda c,e: c.count > 0)
def done(ctx, e):
"""
Outputs the final average to the user.
"""
print("{} samples with average of {}".format(ctx.count, ctx.accumulator / ctx.count))
shutdown()
@event('end-of-input')
@condition(lambda c,e: c.count == 0)
def no_input(ctx, e):
"""
Invoked of no input is given and input is finished.
"""
print("0 samples. \"Does not compute!\"")
shutdown()

40
demos/chat.py Normal file
View File

@@ -0,0 +1,40 @@
from eca import *
import datetime
import eca.http
# add message posting handler
def add_request_handlers(httpd):
httpd.add_route('/api/message', eca.http.GenerateEvent('incoming'), methods=['POST'])
# use the library content from the template_static dir instead of our own
# this is a bit finicky, since execution now depends on a proper working directory.
httpd.add_content('/lib/', 'template_static/lib')
httpd.add_content('/style/', 'template_static/style')
# store name of context
@event('init')
def setup(ctx, e):
ctx.name = e.data['name']
# emit incoming messages to the client
@event('message')
def on_message(ctx, e):
name = e.data['name']
text = e.data['text']
time = e.data['time'].strftime('%Y-%m-%d %H:%M:%S')
emit('message',{
'text': "{} @{}: {}".format(name, time, text)
})
# do a global fire for each message from the client
@event('incoming')
def on_incoming(ctx, e):
fire_global('message', {
'name': ctx.name,
'text': e.data['text'],
'time': datetime.datetime.now()
})

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/core.js"></script>
<script src="/lib/form.js"></script>
<script src="/lib/log.js"></script>
</head>
<body class="container_12">
<h1>ECA Chat</h1>
<div id='messages' class='grid_12 vert_4'></div>
<div id='form' class='grid_8 vert_2'>
<form>
<textarea name='text' style='width: 100%'></textarea>
<input type='submit' value='Send message'/>
</form>
</div>
<script>
block('#messages').log();
events.connect('message', '#messages');
block('#form').form({
target: '/api/message',
callback: function() {
$('textarea').val('');
}
});
// small usability tweak
$('textarea').keydown(function(e) {
if((e.which == 10 || e.which == 13) && e.ctrlKey) {
$('#form input[type="submit"]').click();
}
});
</script>
</body>
</html>

52
demos/drinks.py Normal file
View File

@@ -0,0 +1,52 @@
from eca import *
import eca.http
from pprint import pprint
# This function will be called to set up the HTTP server
def add_request_handlers(httpd):
# add an event-generating request handler to fire 'order' events
# This requires the POST method because the event generation handler only
# understands POST requests.
httpd.add_route('/api/order', eca.http.GenerateEvent('order'), methods=['POST'])
# use the library content from the template_static dir instead of our own
# this is a bit finicky, since execution now depends on a proper working directory.
httpd.add_content('/lib/', 'template_static/lib')
httpd.add_content('/style/', 'template_static/style')
@event('order')
def order(c, e):
# we received an order...
# ...go an print it
print("Received a new order:")
pprint(e.data)
# ...and emit it to all interested browsers
# (conceptually, this could also include the Barista's workstation)
emit('orders',{
'text': str(e.data)
});
# Below are some examples of how to handle incoming requests based
# on observed qualities.
# Inform the coffee machine handler that more coffee is required.
@event('order')
@condition(lambda c,e: e.data['drink'] == 'Coffee')
def start_brewing(c,e):
print("-> Start the coffee brewer!")
# Check for a very specific order
@event('order')
@condition(lambda c,e: e.data['drink'] == 'Tea'
and not e.data['additives']
and e.data['type'].lower() == 'earl grey'
and 'hot' in e.data['notes'].lower())
def picard_has_arrived(c,e):
print("-> Captain Picard has arrived.")

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/core.js"></script>
<script src="/lib/log.js"></script>
<script src="/lib/form.js"></script>
</head>
<body class="container_12">
<h1>Hot Drinks Dashboard</h1>
<!-- Add an order log -->
<div id='log' class="grid_6 vert_6"></div>
<!-- Input form -->
<div id='knopjes' class="grid_6 vert_4">
<h3>Your Order...</h3>
<form>
<div>
<select name='drink'>
<option>Coffee</option>
<option>Tea</option>
</select>
<label for='type_box'>Type</label> <input type='text' name='type' id='type_box'/>
</div>
<div>
Extras:
<input type='checkbox' name='additives' value='sugar' id='sugar_box'/> <label for='sugar_box'>+Sugar</label>
<input type='checkbox' name='additives' value='milk' id='milk_box'/> <label for='milk_box'>+Milk</label>
</div>
<div>
<label for="notes_box">Notes:</label>
<textarea name='notes' id='notes_box' style="width:100%"></textarea>
</div>
<div>
<input type='radio' name='when' id='when_now' value='now'/><label for='when_now'>Now</label><br>
<input type='radio' name='when' id='when_later' value='later'/><label for='when_later'>Later</label>
</div>
<input type='submit' value='Place order'/>
</form>
</div>
<script>
// connect order log to 'orders' emit
block('#log').log();
events.connect('orders', '#log');
// target the form at the exposed API
block('#knopjes').form({
target: '/api/order'
});
</script>
</body>
</html>

94
demos/rolling_chart.py Normal file
View File

@@ -0,0 +1,94 @@
from eca import *
import random
# This function will be called to set up the HTTP server
def add_request_handlers(httpd):
# use the library content from the template_static dir instead of our own
# this is a bit finicky, since execution now depends on a proper working directory.
httpd.add_content('/lib/', 'template_static/lib')
httpd.add_content('/style/', 'template_static/style')
# binds the 'setup' function as the action for the 'init' event
# the action will be called with the context and the event
@event('init')
def setup(ctx, e):
ctx.count = 0
ctx.samples = {
'sensor0': 0.0,
'sensor1': 0.0
}
fire('sample', {
'previous': 0.0,
'name': 'sensor0',
'failure-chance': 0.0,
'reboot-chance': 1.0,
'delay': 0.05
})
fire('sample', {
'previous': 0.0,
'name': 'sensor1',
'failure-chance': 0.05,
'reboot-chance': 0.1,
'delay': 0.05
})
fire('sample', {
'previous': None,
'name': 'sensor2',
'failure-chance': 0.2,
'reboot-chance': 0.8,
'delay': 0.1
})
fire('tick')
# define a normal Python function
def clip(lower, value, upper):
return max(lower, min(value, upper))
@event('sample')
@condition(lambda c,e: e.get('previous') is not None)
def generate_sample(ctx, e):
sample = e.get('previous')
# failure chance off...
if e.get('failure-chance') > random.random():
sample = None
del ctx.samples[e.get('name')]
else:
# base sample on previous one
sample = clip(-100, e.get('previous') + random.uniform(+5.0, -5.0), 100)
ctx.samples[e.get('name')] = sample
# chain event
data = dict(e.data)
data.update({'previous': sample})
fire('sample', data, delay=e.get('delay'))
@event('sample')
@condition(lambda c,e: e.get('previous') is None)
def try_reboot(ctx, e):
sample = e.get('previous')
if e.get('reboot-chance') > random.random():
sample = random.uniform(100,-100)
ctx.samples[e.get('name')] = sample
data = dict(e.data)
data.update({'previous': sample})
fire('sample', data, delay=e.get('delay'))
@event('tick')
def tick(ctx, e):
# emit to outside world
emit('sample',{
'action': 'add',
'values': ctx.samples
})
fire('tick', delay=0.05);

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/core.js"></script>
<script src="/lib/charts.js"></script>
</head>
<body class="container_12">
<h1>Rolling Chart Dashboard</h1>
<div id="graph0" class="grid_12 vert_5"></div>
<div id="graph1" class="grid_12 vert_5"></div>
<script>
// create a rolling chart block
block('#graph0').rolling_chart({
memory: 75,
series: {
'sensor0': {},
'sensor1': {}
},
chart: {
yaxis:{
min: -100,
max: 100
},
xaxis: {
show: false
}
}
});
block('#graph1').rolling_chart({
memory: 150,
series: {
'sensor0': {},
'sensor1': {},
'sensor2': {
lines: {
fill: true,
steps: true
}
}
},
chart: {
yaxis:{
min: -100,
max: 100
},
xaxis: {
show: false
}
}
});
// connect sample event to graph
events.connect('sample', '#graph0');
events.connect('sample', '#graph1');
</script>
</body>
</html>

52
demos/shout.js Normal file
View File

@@ -0,0 +1,52 @@
/*
* This file is the demo for a block definition. For more information
* see:
* https://github.com/utwente-db/eca/wiki/Extending:-Creating-Your-Own-Blocks
*
*/
(function($, block) {
block.fn.shout = function(config) {
// handle configuration
var options = $.extend({
size: '64pt',
text: 'RED',
color: 'red'
}, config);
// create HTML representation
var $el = $('<span></span>').appendTo(this.$element);
$el.css('font-size', options.size);
// create HTML element for display
var data = {
text: options.text,
color: options.color
}
// update function to update element
var update = function() {
$el.text(data.text+'!').css('color', data.color);
}
// invoke update to initialise the display
update();
// register actions
this.actions({
word: function(e, message) {
data.text = message.text;
update();
},
color: function(e, message) {
data.color = message.color;
update();
}
});
// return the element for further work
return this.$element;
}
})(jQuery, block);

25
demos/tweet_arff.py Normal file
View File

@@ -0,0 +1,25 @@
from eca import *
from eca.generators import start_offline_tweets
import datetime
import textwrap
@event('init')
def setup(ctx, e):
# start the offline tweet stream
start_offline_tweets('data/batatweets.txt', 'chirp', time_factor=10000, arff_file='data/batatweets.arff')
@event('chirp')
def tweet(ctx, e):
# we receive a tweet
tweet = e.data
# parse date
time = datetime.datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
# nicify text
text = textwrap.fill(tweet['text'],initial_indent=' ', subsequent_indent=' ')
# generate output
output = "[{}] {} (@{}) +{}:\n{}".format(time, tweet['user']['name'], tweet['user']['screen_name'], tweet['extra']['@@class@@'], text)
emit('tweet', output)

25
demos/tweet_rules.py Normal file
View File

@@ -0,0 +1,25 @@
from eca import *
from eca.generators import start_offline_tweets
import datetime
import textwrap
@event('init')
def setup(ctx, e):
# start the offline tweet stream
start_offline_tweets('data/batatweets.txt', 'chirp', time_factor=10000)
@event('chirp')
def tweet(ctx, e):
# we receive a tweet
tweet = e.data
# parse date
time = datetime.datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
# nicify text
text = textwrap.fill(tweet['text'],initial_indent=' ', subsequent_indent=' ')
# generate output
output = "[{}] {} (@{}):\n{}".format(time, tweet['user']['name'], tweet['user']['screen_name'], text)
emit('tweet', output)

53
demos/wordcloud.py Normal file
View File

@@ -0,0 +1,53 @@
from eca import *
from eca.generators import start_offline_tweets
import datetime
import textwrap
import pprint
import re
# This function will be called to set up the HTTP server
def add_request_handlers(httpd):
# use the library content from the template_static dir instead of our own
# this is a bit finicky, since execution now depends on a proper working directory.
httpd.add_content('/lib/', 'template_static/lib')
httpd.add_content('/style/', 'template_static/style')
@event('init')
def setup(ctx, e):
# start the offline tweet stream
start_offline_tweets('data/bata_2014.txt', 'chirp', time_factor=100000)
ctx.words = {}
# simple word splitter
pattern = re.compile('\W+')
# sample stopword list, needs to be much more sophisticated
stopwords = ['het', 'een', 'aan', 'zijn', 'http', 'www', 'com', 'ben', 'jij']
def words(message):
result = pattern.split(message)
result = map(lambda w: w.lower(), result)
result = filter(lambda w: w not in stopwords, result)
result = filter(lambda w: len(w) > 2, result)
return result
@event('chirp')
def tweet(ctx, e):
# we receive a tweet
tweet = e.data
for w in words(tweet['text']):
emit('word', {
'action': 'add',
'value': (w, 1)
})
emit('taart', {
'action': 'add',
'value': (str(w[0]), 1)
})
emit('balk', {
'action': 'add',
'value': (str(w[0]), 1)
})

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<link rel="stylesheet" href="/style/wordcloud.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/jquery.flot.categories.min.js"></script>
<script src="/lib/jquery.flot.pie.js"></script>
<script src="/lib/core.js"></script>
<script src="/lib/charts.js"></script>
<script src="/lib/jqcloud-1.0.4.js"></script>
<script src="/lib/wordcloud.js"></script>
<script src="/lib/tweets.js"></script>
</head>
<body class="container_12">
<h1>Word Cloud Dashboard</h1>
<div id="wolk" class="grid_12 vert_4"></div>
<div id="taart" class="grid_6 vert_4"></div>
<div id="balk" class="grid_6 vert_4"></div>
<script>
block('#wolk').wordcloud({
filter_function: function(cat, val, max) {
return val >= 3; // do not display words seen less than 3 times
}
});
events.connect('word', '#wolk');
block('#taart').piechart();
events.connect('taart', '#taart');
block('#balk').barchart();
events.connect('balk', '#balk');
</script>
</body>
</html>

55
devjan.py Normal file
View File

@@ -0,0 +1,55 @@
from eca import *
import random
## You might have to update the root path to point to the correct path
## (by default, it points to <rules>_static)
# root_content_path = 'template_static'
# binds the 'setup' function as the action for the 'init' event
# the action will be called with the context and the event
@event('init')
def setup(ctx, e):
ctx.count = 0
fire('sample', {'previous': 0.0})
# define a normal Python function
def clip(lower, value, upper):
return max(lower, min(value, upper))
@event('sample')
def generate_sample(ctx, e):
ctx.count += 1
if ctx.count % 50 == 0:
emit('debug', {'text': 'Log message #'+str(ctx.count)+'!'})
emit('barsample', {'action': 'reset'})
# base sample on previous one
sample = random.uniform(+5.0, -5.0)
li = int(random.uniform(+5.0,0))+1
l = 'cat'+str(li)
v = random.uniform(+10.0,0)
# emit to outside world
# emit('linesample',{
# 'action': 'add',
# 'series' : 'lineA',
# 'value': [[ctx.count,random.uniform(+10.0,0)]]
# })
# emit('linesample',{
# 'action': 'add',
# 'series' : 'lineB',
# 'value': [[ctx.count,random.uniform(+10.0,0)]]
# })
emit('barsample',{ 'action': 'set', 'series': 'serie'+str(int(random.uniform(1,4))), 'value': [l,v] })
emit('wordsample',{
'action': 'add',
'value': [l,v]
})
# chain event
fire('sample', {'previous': sample}, delay=0.05)

100
devjan_static/index.html Normal file
View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/jquery.flot.categories.min.js"></script>
<script src="/lib/jquery.flot.pie.js"></script>
<script src="/lib/events.js"></script>
<script src="/lib/blocks.js"></script>
<script src="/lib/charts.js"></script>
<script src="/lib/piecharts.js"></script>
<script src="/lib/barcharts.js"></script>
<script src="/lib/log.js"></script>
<link rel="stylesheet" type="text/css" href="lib/jqcloud.css" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js"></script>
<script type="text/javascript" src="lib/jqcloud-1.0.4.js"></script>
<script src="/lib/wordcloud.js"></script>
</head>
<body class="container_12">
<h1>ECA Dashboard Template</h1>
<div class="grid_6 vert_4">
<p>This is the dashboard template file. The easiest way to get started is to think up a simple name (let's say we take 'dashboard'). Now copy <code>template.py</code> to <code>{name}.py</code> start a new module (so that's <code>dashboard.py</code>) and copy <code>template_static</code> to <code>{name}_static</code>.
<p>Now you can run the new project with: <pre>python neca.py -s {name}.py</pre>
<p>Further documentation on the ECA system can be found at <a href="https://github.com/utwente-db/eca/wiki">github.com/utwente-db/eca/wiki</a>, and demos can be found in the <code>demos/</code> directory.
</div>
<div class="grid_6 vert_4">
<p>In the sample <code>template.py</code> (which comes with the dashboard you're looking at right now), you will find the rules that power this example.
<p>Rules are written in <a href="https://www.python.org/">Python</a> and work as follows:
<pre>@event("foo")
def action(context, event):
print("Event " + event.name + "!")
</pre>
The <code>@event</code> part tells the system to fire the action whenever the event 'foo' occurs. The <code>def action(context, event):</code> part defines a new action that takes two arguments: the context and the event. The rest of the code is the action body.
</div>
<div class="clear"></div>
<div class="grid_4">
<p>The graph to the right is continuously filled with data generated by the rules.
<p>In <code>template.py</code> you can see that an event called 'sample' is fired again and again to create new data points for the graph.
<p>These points are then sent to the browser with:
<pre>emit('sample',{
'action': 'add',
'value': sample
})</pre>
</div>
<div id="bargraph" class="grid_8 vert_4"></div>
<script>
// create a barchart chart block
block('#bargraph').barchart({
bar_options:
{
series: {
bar: { show: true }
},
bars: {
align: "center",
barWidth: 0.5
}
}
});
// connect sample event to graph
events.connect('barsample', '#bargraph');
</script>
<div id="wordcloud" class="grid_8 vert_4"></div>
<script>
// create a wordcloud block
block('#wordcloud').wordcloud({
word_options:
{
series: {
pie: { show: true }
},
legend: { show: false }
}
});
// connect sample event to graph
events.connect('wordsample', '#wordcloud');
</script>
// <div id="my_favorite_latin_words" class="grid_12 vert_8"></div>
</body>
</html>

View File

@@ -0,0 +1,108 @@
(function($, block) {
// a simple barchart example
block.fn.barchart = function(config) {
var options = $.extend({
series : { "serie1":{
data: {"January": 10, "February": 8, "March": 4, "April": 13, "May": 20, "June": 9},
label: "serie 1",
bars: {
show: true,
barWidth: 0.2,
align: "left"
}
}, "serie2":{
data: {"January": 10, "February": 8, "March": 4, "April": 13, "May": 20, "June": 9},
label: "series 2",
bars: {
show: true,
barWidth: 0.2,
align: "center"
}
}, "serie3":{
data: {"January": 10, "February": 8, "March": 4, "April": 13, "May": 20, "June": 9},
label: "series 3",
bars: {
show: true,
barWidth: 0.2,
align: "right"
}
}}
}, config);
var bar_init = {
xaxis: {
mode: "categories",
tickLength: 0
}
}
var bardata_series = options.series;
var translate_bar = function() {
var result = [];
for(var k in bardata_series) {
if (bardata_series.hasOwnProperty(k)) {
var newserie = jQuery.extend({}, bardata_series[k]);
var newdata = [];
var data = newserie.data;
for(var l in data) {
if (data.hasOwnProperty(l)) {
newdata.push([l,data[l]]);
}
}
newserie.data = newdata;
result.push(newserie);
}
}
return result;
}
var plot = $.plot(this.$element, translate_bar(), bar_init);
var addbar = function(serie_label, category, value) {
var data = bardata_series[serie_label].data;
if (data.hasOwnProperty(category))
data[category] = (data[category] + value);
else
data[category] = value;
redraw();
}
var setbar = function(serie_label, category, value) {
var data = bardata_series[serie_label].data;
data[category] = value;
redraw();
}
var redraw = function() {
plot.setData(translate_bar());
plot.setupGrid();
plot.draw();
}
var reset = function() {
for(var k in bardata_series) {
if (bardata_series.hasOwnProperty(k)) {
bardata_series[k].data = {};
}
}
}
this.actions({
'set': function(e, message) {
setbar(message.series,message.value[0],message.value[1]);
},
'add': function(e, message) {
addbar(message.series,message.value[0],message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,68 @@
/*
** Blocks plugin allows for quick creation of new block types.
*/
(function($) {
var ConstructionState = function($element) {
this.$element = $element;
// transfer all block constructors to scope
for(var b in block.fn) {
// prevent overrides
if(!(b in this)) {
// reference block type in this object
this[b] = block.fn[b];
}
}
};
ConstructionState.prototype.actions = function(actions_or_def, def) {
// handle function overloading
if(typeof actions_or_def == 'function') {
def = actions_or_def;
actions = {};
} else {
actions = actions_or_def;
}
// default actionless handler
if(typeof def == 'undefined') {
def = function(e, message) {
console.error("Received actionless server event." +
" Did you forget to set an action field?");
}
}
// dispatch all incoming server events
this.$element.on('server-event', function(e, message) {
if(!('action' in message)) {
$(this).trigger('_default.server-event', [message]);
} else {
$(this).trigger(message.action+'.server-event', [message]);
}
});
// bind all actions
this.$element.on('_default.server-event', def);
for(var k in actions) {
this.$element.on(k+'.server-event', actions[k]);
}
}
block = function(elements) {
// allow passing of selectors, jquery objects and DOM nodes
var $element = $(elements);
// actual work
if($element.length != 1) {
console.error("Must have one element to create block for." +
" Was given: '",elements,"'");
return;
}
return new ConstructionState($element);
}
block.fn = {};
})(jQuery);

View File

@@ -0,0 +1,78 @@
(function($, block) {
// a simple rolling chart with memory
block.fn.rolling_chart = function(config) {
var options = $.extend({
memory: 100,
series: { serie : {label:"serie", color:'black'} }
}, config);
var handle_data = function(values) {
var result = [];
for(var i in values) {
result.push([i, values[i]]);
}
return result;
};
var xo = { series: {
lines: { show: true },
points: {
radius: 3,
show: true,
fill: true
}
}};
var plot = $.plot(this.$element, [] , {});
var reset = function() {
var result = options.series;
for(var k in result) {
if (result.hasOwnProperty(k)) {
result[k].databuffer = [];
}
}
return result;
}
var plot_series = reset();
var add_to_serie = function(skey,value) {
var databuffer = plot_series[skey].databuffer;
if(databuffer.length > options.memory) {
plot_series[skey].databuffer = databuffer.slice(1);
}
databuffer.push(value);
}
var redraw = function(serie_value) {
var plot_current = [];
var mykeys = Object.keys(plot_series);
for(var mykey in mykeys) {
var skey = mykeys[mykey];
var serie = plot_series[skey];
// serie['databuffer'].push(serie_value[skey]);
add_to_serie(skey,serie_value[skey]);
serie['data'] = handle_data(serie['databuffer']);
plot_current.push(serie);
}
plot.setData(plot_current);
plot.setupGrid();
plot.draw();
}
this.actions({
'add': function(e, message) {
redraw(message.value);
},
'reset': function(e, message) {
plot_series = reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,38 @@
/*
Event stream handling.
See https://developer.mozilla.org/en-US/docs/Web/API/EventSource for a more
comprehensive explanation.
*/
events = {};
(function($, exports) {
var e = new EventSource('/events');
exports.connect = function(name, elements) {
// wrap to allow selector, jQuery object and DOM nodes
var $elements = $(elements);
// add listener that triggers events in DOM
this.listen(name, function(message) {
$elements.trigger('server-event', [message]);
});
};
exports.listen = function(name, callback) {
// add event listener to event stream
e.addEventListener(name, function(m) {
try {
var message = JSON.parse(m.data);
} catch(err) {
console.exception("Received malformed message: ",err);
return;
}
callback(message);
});
};
})(jQuery, events);

View File

@@ -0,0 +1,232 @@
/*!
* jQCloud Plugin for jQuery
*
* Version 1.0.4
*
* Copyright 2011, Luca Ongaro
* Licensed under the MIT license.
*
* Date: 2013-05-09 18:54:22 +0200
*/
(function( $ ) {
"use strict";
$.fn.jQCloud = function(word_array, options) {
// Reference to the container element
var $this = this;
// Namespace word ids to avoid collisions between multiple clouds
var cloud_namespace = $this.attr('id') || Math.floor((Math.random()*1000000)).toString(36);
// Default options value
var default_options = {
width: $this.width(),
height: $this.height(),
center: {
x: ((options && options.width) ? options.width : $this.width()) / 2.0,
y: ((options && options.height) ? options.height : $this.height()) / 2.0
},
delayedMode: word_array.length > 50,
shape: false, // It defaults to elliptic shape
encodeURI: true,
removeOverflowing: true
};
options = $.extend(default_options, options || {});
// Add the "jqcloud" class to the container for easy CSS styling, set container width/height
$this.addClass("jqcloud").width(options.width).height(options.height);
// Container's CSS position cannot be 'static'
if ($this.css("position") === "static") {
$this.css("position", "relative");
}
var drawWordCloud = function() {
// Helper function to test if an element overlaps others
var hitTest = function(elem, other_elems) {
// Pairwise overlap detection
var overlapping = function(a, b) {
if (Math.abs(2.0*a.offsetLeft + a.offsetWidth - 2.0*b.offsetLeft - b.offsetWidth) < a.offsetWidth + b.offsetWidth) {
if (Math.abs(2.0*a.offsetTop + a.offsetHeight - 2.0*b.offsetTop - b.offsetHeight) < a.offsetHeight + b.offsetHeight) {
return true;
}
}
return false;
};
var i = 0;
// Check elements for overlap one by one, stop and return false as soon as an overlap is found
for(i = 0; i < other_elems.length; i++) {
if (overlapping(elem, other_elems[i])) {
return true;
}
}
return false;
};
// Make sure every weight is a number before sorting
for (var i = 0; i < word_array.length; i++) {
word_array[i].weight = parseFloat(word_array[i].weight, 10);
}
// Sort word_array from the word with the highest weight to the one with the lowest
word_array.sort(function(a, b) { if (a.weight < b.weight) {return 1;} else if (a.weight > b.weight) {return -1;} else {return 0;} });
var step = (options.shape === "rectangular") ? 18.0 : 2.0,
already_placed_words = [],
aspect_ratio = options.width / options.height;
// Function to draw a word, by moving it in spiral until it finds a suitable empty place. This will be iterated on each word.
var drawOneWord = function(index, word) {
// Define the ID attribute of the span that will wrap the word, and the associated jQuery selector string
var word_id = cloud_namespace + "_word_" + index,
word_selector = "#" + word_id,
angle = 6.28 * Math.random(),
radius = 0.0,
// Only used if option.shape == 'rectangular'
steps_in_direction = 0.0,
quarter_turns = 0.0,
weight = 5,
custom_class = "",
inner_html = "",
word_span;
// Extend word html options with defaults
word.html = $.extend(word.html, {id: word_id});
// If custom class was specified, put them into a variable and remove it from html attrs, to avoid overwriting classes set by jQCloud
if (word.html && word.html["class"]) {
custom_class = word.html["class"];
delete word.html["class"];
}
// Check if min(weight) > max(weight) otherwise use default
if (word_array[0].weight > word_array[word_array.length - 1].weight) {
// Linearly map the original weight to a discrete scale from 1 to 10
weight = Math.round((word.weight - word_array[word_array.length - 1].weight) /
(word_array[0].weight - word_array[word_array.length - 1].weight) * 9.0) + 1;
}
word_span = $('<span>').attr(word.html).addClass('w' + weight + " " + custom_class);
// Append link if word.url attribute was set
if (word.link) {
// If link is a string, then use it as the link href
if (typeof word.link === "string") {
word.link = {href: word.link};
}
// Extend link html options with defaults
if ( options.encodeURI ) {
word.link = $.extend(word.link, { href: encodeURI(word.link.href).replace(/'/g, "%27") });
}
inner_html = $('<a>').attr(word.link).text(word.text);
} else {
inner_html = word.text;
}
word_span.append(inner_html);
// Bind handlers to words
if (!!word.handlers) {
for (var prop in word.handlers) {
if (word.handlers.hasOwnProperty(prop) && typeof word.handlers[prop] === 'function') {
$(word_span).bind(prop, word.handlers[prop]);
}
}
}
$this.append(word_span);
var width = word_span.width(),
height = word_span.height(),
left = options.center.x - width / 2.0,
top = options.center.y - height / 2.0;
// Save a reference to the style property, for better performance
var word_style = word_span[0].style;
word_style.position = "absolute";
word_style.left = left + "px";
word_style.top = top + "px";
while(hitTest(word_span[0], already_placed_words)) {
// option shape is 'rectangular' so move the word in a rectangular spiral
if (options.shape === "rectangular") {
steps_in_direction++;
if (steps_in_direction * step > (1 + Math.floor(quarter_turns / 2.0)) * step * ((quarter_turns % 4 % 2) === 0 ? 1 : aspect_ratio)) {
steps_in_direction = 0.0;
quarter_turns++;
}
switch(quarter_turns % 4) {
case 1:
left += step * aspect_ratio + Math.random() * 2.0;
break;
case 2:
top -= step + Math.random() * 2.0;
break;
case 3:
left -= step * aspect_ratio + Math.random() * 2.0;
break;
case 0:
top += step + Math.random() * 2.0;
break;
}
} else { // Default settings: elliptic spiral shape
radius += step;
angle += (index % 2 === 0 ? 1 : -1)*step;
left = options.center.x - (width / 2.0) + (radius*Math.cos(angle)) * aspect_ratio;
top = options.center.y + radius*Math.sin(angle) - (height / 2.0);
}
word_style.left = left + "px";
word_style.top = top + "px";
}
// Don't render word if part of it would be outside the container
if (options.removeOverflowing && (left < 0 || top < 0 || (left + width) > options.width || (top + height) > options.height)) {
word_span.remove()
return;
}
already_placed_words.push(word_span[0]);
// Invoke callback if existing
if ($.isFunction(word.afterWordRender)) {
word.afterWordRender.call(word_span);
}
};
var drawOneWordDelayed = function(index) {
index = index || 0;
if (!$this.is(':visible')) { // if not visible then do not attempt to draw
setTimeout(function(){drawOneWordDelayed(index);},10);
return;
}
if (index < word_array.length) {
drawOneWord(index, word_array[index]);
setTimeout(function(){drawOneWordDelayed(index + 1);}, 10);
} else {
if ($.isFunction(options.afterCloudRender)) {
options.afterCloudRender.call($this);
}
}
};
// Iterate drawOneWord on every word. The way the iteration is done depends on the drawing mode (delayedMode is true or false)
if (options.delayedMode){
drawOneWordDelayed();
}
else {
$.each(word_array, drawOneWord);
if ($.isFunction(options.afterCloudRender)) {
options.afterCloudRender.call($this);
}
}
};
// Delay execution so that the browser can render the page before the computatively intensive word cloud drawing
setTimeout(function(){drawWordCloud();}, 10);
return $this;
};
})(jQuery);

View File

@@ -0,0 +1,49 @@
/* fonts */
div.jqcloud {
font-family: "Helvetica", "Arial", sans-serif;
font-size: 10px;
line-height: normal;
}
div.jqcloud a {
font-size: inherit;
text-decoration: none;
}
div.jqcloud span.w10 { font-size: 550%; }
div.jqcloud span.w9 { font-size: 500%; }
div.jqcloud span.w8 { font-size: 450%; }
div.jqcloud span.w7 { font-size: 400%; }
div.jqcloud span.w6 { font-size: 350%; }
div.jqcloud span.w5 { font-size: 300%; }
div.jqcloud span.w4 { font-size: 250%; }
div.jqcloud span.w3 { font-size: 200%; }
div.jqcloud span.w2 { font-size: 150%; }
div.jqcloud span.w1 { font-size: 100%; }
/* colors */
div.jqcloud { color: #09f; }
div.jqcloud a { color: inherit; }
div.jqcloud a:hover { color: #0df; }
div.jqcloud a:hover { color: #0cf; }
div.jqcloud span.w10 { color: #0cf; }
div.jqcloud span.w9 { color: #0cf; }
div.jqcloud span.w8 { color: #0cf; }
div.jqcloud span.w7 { color: #39d; }
div.jqcloud span.w6 { color: #90c5f0; }
div.jqcloud span.w5 { color: #90a0dd; }
div.jqcloud span.w4 { color: #90c5f0; }
div.jqcloud span.w3 { color: #a0ddff; }
div.jqcloud span.w2 { color: #99ccee; }
div.jqcloud span.w1 { color: #aab5f0; }
/* layout */
div.jqcloud {
overflow: hidden;
position: relative;
}
div.jqcloud span { padding: 0; }

4
devjan_static/lib/jquery-2.1.1.min.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

8
devjan_static/lib/jquery.flot.min.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,820 @@
/* Flot plugin for rendering pie charts.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin assumes that each series has a single data value, and that each
value is a positive integer or zero. Negative numbers don't make sense for a
pie chart, and have unpredictable results. The values do NOT need to be
passed in as percentages; the plugin will calculate the total and per-slice
percentages internally.
* Created by Brian Medendorp
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
The plugin supports these options:
series: {
pie: {
show: true/false
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
offset: {
top: integer value to move the pie up or down
left: integer value to move the pie left or right, or 'auto'
},
stroke: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
width: integer pixel width of the stroke
},
label: {
show: true/false, or 'auto'
formatter: a user-defined function that modifies the text/style of the label text
radius: 0-1 for percentage of fullsize, or a specified pixel length
background: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
opacity: 0-1
},
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
},
combine: {
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
label: any text value of what the combined slice should be labeled
}
highlight: {
opacity: 0-1
}
}
}
More detail and specific examples can be found in the included HTML file.
*/
(function($) {
// Maximum redraw attempts when fitting labels within the plot
var REDRAW_ATTEMPTS = 10;
// Factor by which to shrink the pie when fitting labels within the plot
var REDRAW_SHRINK = 0.95;
function init(plot) {
var canvas = null,
target = null,
options = null,
maxRadius = null,
centerLeft = null,
centerTop = null,
processed = false,
ctx = null;
// interactive variables
var highlights = [];
// add hook to determine if pie plugin in enabled, and then perform necessary operations
plot.hooks.processOptions.push(function(plot, options) {
if (options.series.pie.show) {
options.grid.show = false;
// set labels.show
if (options.series.pie.label.show == "auto") {
if (options.legend.show) {
options.series.pie.label.show = false;
} else {
options.series.pie.label.show = true;
}
}
// set radius
if (options.series.pie.radius == "auto") {
if (options.series.pie.label.show) {
options.series.pie.radius = 3/4;
} else {
options.series.pie.radius = 1;
}
}
// ensure sane tilt
if (options.series.pie.tilt > 1) {
options.series.pie.tilt = 1;
} else if (options.series.pie.tilt < 0) {
options.series.pie.tilt = 0;
}
}
});
plot.hooks.bindEvents.push(function(plot, eventHolder) {
var options = plot.getOptions();
if (options.series.pie.show) {
if (options.grid.hoverable) {
eventHolder.unbind("mousemove").mousemove(onMouseMove);
}
if (options.grid.clickable) {
eventHolder.unbind("click").click(onClick);
}
}
});
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
var options = plot.getOptions();
if (options.series.pie.show) {
processDatapoints(plot, series, data, datapoints);
}
});
plot.hooks.drawOverlay.push(function(plot, octx) {
var options = plot.getOptions();
if (options.series.pie.show) {
drawOverlay(plot, octx);
}
});
plot.hooks.draw.push(function(plot, newCtx) {
var options = plot.getOptions();
if (options.series.pie.show) {
draw(plot, newCtx);
}
});
function processDatapoints(plot, series, datapoints) {
if (!processed) {
processed = true;
canvas = plot.getCanvas();
target = $(canvas).parent();
options = plot.getOptions();
plot.setData(combine(plot.getData()));
}
}
function combine(data) {
var total = 0,
combined = 0,
numCombined = 0,
color = options.series.pie.combine.color,
newdata = [];
// Fix up the raw data from Flot, ensuring the data is numeric
for (var i = 0; i < data.length; ++i) {
var value = data[i].data;
// If the data is an array, we'll assume that it's a standard
// Flot x-y pair, and are concerned only with the second value.
// Note how we use the original array, rather than creating a
// new one; this is more efficient and preserves any extra data
// that the user may have stored in higher indexes.
if ($.isArray(value) && value.length == 1) {
value = value[0];
}
if ($.isArray(value)) {
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
value[1] = +value[1];
} else {
value[1] = 0;
}
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
value = [1, +value];
} else {
value = [1, 0];
}
data[i].data = [value];
}
// Sum up all the slices, so we can calculate percentages for each
for (var i = 0; i < data.length; ++i) {
total += data[i].data[0][1];
}
// Count the number of slices with percentages below the combine
// threshold; if it turns out to be just one, we won't combine.
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (value / total <= options.series.pie.combine.threshold) {
combined += value;
numCombined++;
if (!color) {
color = data[i].color;
}
}
}
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
newdata.push(
$.extend(data[i], { /* extend to allow keeping all other original data values
and using them e.g. in labelFormatter. */
data: [[1, value]],
color: data[i].color,
label: data[i].label,
angle: value * Math.PI * 2 / total,
percent: value / (total / 100)
})
);
}
}
if (numCombined > 1) {
newdata.push({
data: [[1, combined]],
color: color,
label: options.series.pie.combine.label,
angle: combined * Math.PI * 2 / total,
percent: combined / (total / 100)
});
}
return newdata;
}
function draw(plot, newCtx) {
if (!target) {
return; // if no series were passed
}
var canvasWidth = plot.getPlaceholder().width(),
canvasHeight = plot.getPlaceholder().height(),
legendWidth = target.children().filter(".legend").children().width() || 0;
ctx = newCtx;
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
// When combining smaller slices into an 'other' slice, we need to
// add a new series. Since Flot gives plugins no way to modify the
// list of series, the pie plugin uses a hack where the first call
// to processDatapoints results in a call to setData with the new
// list of series, then subsequent processDatapoints do nothing.
// The plugin-global 'processed' flag is used to control this hack;
// it starts out false, and is set to true after the first call to
// processDatapoints.
// Unfortunately this turns future setData calls into no-ops; they
// call processDatapoints, the flag is true, and nothing happens.
// To fix this we'll set the flag back to false here in draw, when
// all series have been processed, so the next sequence of calls to
// processDatapoints once again starts out with a slice-combine.
// This is really a hack; in 0.9 we need to give plugins a proper
// way to modify series before any processing begins.
processed = false;
// calculate maximum radius and center point
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
centerLeft = canvasWidth / 2;
if (options.series.pie.offset.left == "auto") {
if (options.legend.position.match("w")) {
centerLeft += legendWidth / 2;
} else {
centerLeft -= legendWidth / 2;
}
if (centerLeft < maxRadius) {
centerLeft = maxRadius;
} else if (centerLeft > canvasWidth - maxRadius) {
centerLeft = canvasWidth - maxRadius;
}
} else {
centerLeft += options.series.pie.offset.left;
}
var slices = plot.getData(),
attempts = 0;
// Keep shrinking the pie's radius until drawPie returns true,
// indicating that all the labels fit, or we try too many times.
do {
if (attempts > 0) {
maxRadius *= REDRAW_SHRINK;
}
attempts += 1;
clear();
if (options.series.pie.tilt <= 0.8) {
drawShadow();
}
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
if (attempts >= REDRAW_ATTEMPTS) {
clear();
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
}
if (plot.setSeries && plot.insertLegend) {
plot.setSeries(slices);
plot.insertLegend();
}
// we're actually done at this point, just defining internal functions at this point
function clear() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
target.children().filter(".pieLabel, .pieLabelBackground").remove();
}
function drawShadow() {
var shadowLeft = options.series.pie.shadow.left;
var shadowTop = options.series.pie.shadow.top;
var edge = 10;
var alpha = options.series.pie.shadow.alpha;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
return; // shadow would be outside canvas, so don't draw it
}
ctx.save();
ctx.translate(shadowLeft,shadowTop);
ctx.globalAlpha = alpha;
ctx.fillStyle = "#000";
// center and rotate to starting position
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//radius -= edge;
for (var i = 1; i <= edge; i++) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
ctx.fill();
radius -= i;
}
ctx.restore();
}
function drawPie() {
var startAngle = Math.PI * options.series.pie.startAngle;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
// center and rotate to starting position
ctx.save();
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
// draw slices
ctx.save();
var currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
slices[i].startAngle = currentAngle;
drawSlice(slices[i].angle, slices[i].color, true);
}
ctx.restore();
// draw slice outlines
if (options.series.pie.stroke.width > 0) {
ctx.save();
ctx.lineWidth = options.series.pie.stroke.width;
currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
}
ctx.restore();
}
// draw donut hole
drawDonutHole(ctx);
ctx.restore();
// Draw the labels, returning true if they fit within the plot
if (options.series.pie.label.show) {
return drawLabels();
} else return true;
function drawSlice(angle, color, fill) {
if (angle <= 0 || isNaN(angle)) {
return;
}
if (fill) {
ctx.fillStyle = color;
} else {
ctx.strokeStyle = color;
ctx.lineJoin = "round";
}
ctx.beginPath();
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
ctx.moveTo(0, 0); // Center of the pie
}
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
ctx.closePath();
//ctx.rotate(angle); // This doesn't work properly in Opera
currentAngle += angle;
if (fill) {
ctx.fill();
} else {
ctx.stroke();
}
}
function drawLabels() {
var currentAngle = startAngle;
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
for (var i = 0; i < slices.length; ++i) {
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
if (!drawLabel(slices[i], currentAngle, i)) {
return false;
}
}
currentAngle += slices[i].angle;
}
return true;
function drawLabel(slice, startAngle, index) {
if (slice.data[0][1] == 0) {
return true;
}
// format label text
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
if (lf) {
text = lf(slice.label, slice);
} else {
text = slice.label;
}
if (plf) {
text = plf(text, slice);
}
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
target.append(html);
var label = target.children("#pieLabel" + index);
var labelTop = (y - label.height() / 2);
var labelLeft = (x - label.width() / 2);
label.css("top", labelTop);
label.css("left", labelLeft);
// check to make sure that the label is not outside the canvas
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
return false;
}
if (options.series.pie.label.background.opacity != 0) {
// put in the transparent background separately to avoid blended labels and label boxes
var c = options.series.pie.label.background.color;
if (c == null) {
c = slice.color;
}
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
.css("opacity", options.series.pie.label.background.opacity)
.insertBefore(label);
}
return true;
} // end individual label function
} // end drawLabels function
} // end drawPie function
} // end draw function
// Placed here because it needs to be accessed from multiple locations
function drawDonutHole(layer) {
if (options.series.pie.innerRadius > 0) {
// subtract the center
layer.save();
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
layer.beginPath();
layer.fillStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.fill();
layer.closePath();
layer.restore();
// add inner stroke
layer.save();
layer.beginPath();
layer.strokeStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.stroke();
layer.closePath();
layer.restore();
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
}
}
//-- Additional Interactive related functions --
function isPointInPoly(poly, pt) {
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
&& (c = !c);
return c;
}
function findNearbySlice(mouseX, mouseY) {
var slices = plot.getData(),
options = plot.getOptions(),
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
x, y;
for (var i = 0; i < slices.length; ++i) {
var s = slices[i];
if (s.pie.show) {
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0); // Center of the pie
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
ctx.closePath();
x = mouseX - centerLeft;
y = mouseY - centerTop;
if (ctx.isPointInPath) {
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
} else {
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
var p1X = radius * Math.cos(s.startAngle),
p1Y = radius * Math.sin(s.startAngle),
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
p5X = radius * Math.cos(s.startAngle + s.angle),
p5Y = radius * Math.sin(s.startAngle + s.angle),
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
arrPoint = [x, y];
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
if (isPointInPoly(arrPoly, arrPoint)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
}
ctx.restore();
}
}
return null;
}
function onMouseMove(e) {
triggerClickHoverEvent("plothover", e);
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e);
}
// trigger click or hover event (they send the same parameters so we share their code)
function triggerClickHoverEvent(eventname, e) {
var offset = plot.offset();
var canvasX = parseInt(e.pageX - offset.left);
var canvasY = parseInt(e.pageY - offset.top);
var item = findNearbySlice(canvasX, canvasY);
if (options.grid.autoHighlight) {
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.auto == eventname && !(item && h.series == item.series)) {
unhighlight(h.series);
}
}
}
// highlight the slice
if (item) {
highlight(item.series, eventname);
}
// trigger any hover bind events
var pos = { pageX: e.pageX, pageY: e.pageY };
target.trigger(eventname, [pos, item]);
}
function highlight(s, auto) {
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i == -1) {
highlights.push({ series: s, auto: auto });
plot.triggerRedrawOverlay();
} else if (!auto) {
highlights[i].auto = false;
}
}
function unhighlight(s) {
if (s == null) {
highlights = [];
plot.triggerRedrawOverlay();
}
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i != -1) {
highlights.splice(i, 1);
plot.triggerRedrawOverlay();
}
}
function indexOfHighlight(s) {
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.series == s)
return i;
}
return -1;
}
function drawOverlay(plot, octx) {
var options = plot.getOptions();
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
octx.save();
octx.translate(centerLeft, centerTop);
octx.scale(1, options.series.pie.tilt);
for (var i = 0; i < highlights.length; ++i) {
drawHighlight(highlights[i].series);
}
drawDonutHole(octx);
octx.restore();
function drawHighlight(series) {
if (series.angle <= 0 || isNaN(series.angle)) {
return;
}
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
octx.beginPath();
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
octx.moveTo(0, 0); // Center of the pie
}
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
octx.closePath();
octx.fill();
}
}
} // end init (plugin body)
// define pie specific options and their default values
var options = {
series: {
pie: {
show: false,
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
innerRadius: 0, /* for donut */
startAngle: 3/2,
tilt: 1,
shadow: {
left: 5, // shadow left offset
top: 15, // shadow top offset
alpha: 0.02 // shadow alpha
},
offset: {
top: 0,
left: "auto"
},
stroke: {
color: "#fff",
width: 1
},
label: {
show: "auto",
formatter: function(label, slice) {
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
}, // formatter function
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
background: {
color: null,
opacity: 0
},
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
},
combine: {
threshold: -1, // percentage at which to combine little slices into one larger slice
color: null, // color to give the new slice (auto-generated if null)
label: "Other" // label to give the new slice
},
highlight: {
//color: "#fff", // will add this functionality once parseColor is available
opacity: 0.5
}
}
}
};
$.plot.plugins.push({
init: init,
options: options,
name: "pie",
version: "1.1"
});
})(jQuery);

View File

@@ -0,0 +1,83 @@
(function($, block) {
// a simple linechart example
block.fn.linechart = function(config) {
var options = $.extend({
line_series : ["default"],
line_options : {
series: {
lines: {
show: true
}
}
}}, config);
// create empty linechart with parameter options
var plot = $.plot(this.$element, [],options.line_options);
// dict containing the labels and values
var linedata_series = {};
var initline = function(series) {
for(var k in series) {
linedata_series[series[k]] = {order:k,data:[]};
}
}
initline(options.line_series);
var addline = function(label, values) {
var data;
if (linedata_series.hasOwnProperty(label))
data = linedata_series[label].data;
else
data = linedata_series['default'].data;
for(var v in values) {
data.push(values[v]);
}
redraw();
}
var setline = function(label, values) {
if (linedata_series.hasOwnProperty(label))
linedata_series[label].data = values;
else
linedata_series['default'].data = values;
redraw();
}
var redraw = function() {
var result = [];
for(var k in linedata_series) {
if (linedata_series.hasOwnProperty(k)) {
var line_serie = linedata_series[k];
result.push({label:k,data:line_serie.data});
}
}
plot.setData(result);
plot.setupGrid();
plot.draw();
}
var reset = function() {
initline(options.line_series);
}
this.actions({
'set': function(e, message) {
addline(message.series, message.value);
},
'add': function(e, message) {
addline(message.series, message.value);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

14
devjan_static/lib/log.js Normal file
View File

@@ -0,0 +1,14 @@
(function($, block) {
block.fn.log = function(config) {
this.$element.addClass('block log').append('<ul>');
this.actions(function(e, message){
$ul = $('ul:first-child', this);
$ul.append('<li>');
$ul.find("> li:last-child").text(message.text);
$(this).scrollTop(1000000);
});
return this.$element;
};
})(jQuery, block);

View File

@@ -0,0 +1,64 @@
(function($, block) {
// a simple piechart example
block.fn.piechart = function(config) {
var options = $.extend({
// see: http://www.flotcharts.org/flot/examples/series-pie/
pie_options : {
series: {
pie: {
show: true
}
}
}}, config);
// create empty piechart with parameter options
var plot = $.plot(this.$element, [],options.pie_options);
// dict containing the labels and values
var piedata_dict = {};
var addpie = function(label, value) {
if (piedata_dict.hasOwnProperty(label))
piedata_dict[label] = (piedata_dict[label] + value);
else
piedata_dict[label] = value;
redraw();
}
var setpie = function(label, value) {
piedata_dict[label] = value;
redraw();
}
var redraw = function() {
var result = [];
for(var k in piedata_dict) {
if (piedata_dict.hasOwnProperty(k)) {
result.push({label:k,data:piedata_dict[k]});
}
}
plot.setData(result);
plot.draw();
}
var reset = function() {
piedata_dict = {};
}
this.actions({
'set': function(e, message) {
setpie(message.value[0],message.value[1]);
},
'add': function(e, message) {
addpie(message.value[0],message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,71 @@
(function($, block) {
// a simple wordcloud example
block.fn.wordcloud = function(config) {
var options = $.extend({
// weight=0 means word is not in cloud
weight_function : function(val,max) { return val; },
}, config);
var $container = $(this.$element);
// create empty wordcloud with parameter options
var wordcloud_el = $container.jQCloud([{
text: "TEXT",
weight: 1
}]);
// dict containing the labels and values
var worddata_dict = {};
var addword = function(label, value) {
if (worddata_dict.hasOwnProperty(label)) {
worddata_dict[label] += value;
} else {
worddata_dict[label] = value;
}
redraw();
}
var setword = function(label, value) {
worddata_dict[label] = value;
redraw();
}
var redraw = function() {
var result = [];
var max = 0;
// incomplete, determine max
for (var k in worddata_dict) {
if (worddata_dict.hasOwnProperty(k)) {
max = Math.max(max, worddata_dict[k]);
}
}
for (var k in worddata_dict) {
if (worddata_dict.hasOwnProperty(k)) {
var w = options.weight_function(worddata_dict[k],max);
if ( w > 0 )
result.push({text: k, weight: w});
}
}
$($container).empty().jQCloud(result);
}
var reset = function() {
worddata_dict = {};
}
this.actions({
'set': function(e, message) {
setword(message.value[0], message.value[1]);
},
'add': function(e, message) {
addword(message.value[0], message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,374 @@
/*
Variable Grid System.
Learn more ~ http://www.spry-soft.com/grids/
Based on 960 Grid System - http://960.gs/
Licensed under GPL and MIT.
*/
/*
Forces backgrounds to span full width,
even if there is horizontal scrolling.
Increase this if your layout is wider.
Note: IE6 works fine without this fix.
*/
body {
min-width: 960px;
}
/* Containers
----------------------------------------------------------------------------------------------------*/
.container_12 {
margin-left: auto;
margin-right: auto;
width: 960px;
}
/* Grid >> Global
----------------------------------------------------------------------------------------------------*/
.grid_1,
.grid_2,
.grid_3,
.grid_4,
.grid_5,
.grid_6,
.grid_7,
.grid_8,
.grid_9,
.grid_10,
.grid_11,
.grid_12 {
display:inline;
float: left;
position: relative;
margin-left: 10px;
margin-right: 10px;
}
.push_1, .pull_1,
.push_2, .pull_2,
.push_3, .pull_3,
.push_4, .pull_4,
.push_5, .pull_5,
.push_6, .pull_6,
.push_7, .pull_7,
.push_8, .pull_8,
.push_9, .pull_9,
.push_10, .pull_10,
.push_11, .pull_11,
.push_12, .pull_12 {
position:relative;
}
/* Grid >> Children (Alpha ~ First, Omega ~ Last)
----------------------------------------------------------------------------------------------------*/
.alpha {
margin-left: 0;
}
.omega {
margin-right: 0;
}
/* Grid >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .grid_1 {
width:60px;
}
.container_12 .grid_2 {
width:140px;
}
.container_12 .grid_3 {
width:220px;
}
.container_12 .grid_4 {
width:300px;
}
.container_12 .grid_5 {
width:380px;
}
.container_12 .grid_6 {
width:460px;
}
.container_12 .grid_7 {
width:540px;
}
.container_12 .grid_8 {
width:620px;
}
.container_12 .grid_9 {
width:700px;
}
.container_12 .grid_10 {
width:780px;
}
.container_12 .grid_11 {
width:860px;
}
.container_12 .grid_12 {
width:940px;
}
/* Prefix Extra Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .prefix_1 {
padding-left:80px;
}
.container_12 .prefix_2 {
padding-left:160px;
}
.container_12 .prefix_3 {
padding-left:240px;
}
.container_12 .prefix_4 {
padding-left:320px;
}
.container_12 .prefix_5 {
padding-left:400px;
}
.container_12 .prefix_6 {
padding-left:480px;
}
.container_12 .prefix_7 {
padding-left:560px;
}
.container_12 .prefix_8 {
padding-left:640px;
}
.container_12 .prefix_9 {
padding-left:720px;
}
.container_12 .prefix_10 {
padding-left:800px;
}
.container_12 .prefix_11 {
padding-left:880px;
}
/* Suffix Extra Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .suffix_1 {
padding-right:80px;
}
.container_12 .suffix_2 {
padding-right:160px;
}
.container_12 .suffix_3 {
padding-right:240px;
}
.container_12 .suffix_4 {
padding-right:320px;
}
.container_12 .suffix_5 {
padding-right:400px;
}
.container_12 .suffix_6 {
padding-right:480px;
}
.container_12 .suffix_7 {
padding-right:560px;
}
.container_12 .suffix_8 {
padding-right:640px;
}
.container_12 .suffix_9 {
padding-right:720px;
}
.container_12 .suffix_10 {
padding-right:800px;
}
.container_12 .suffix_11 {
padding-right:880px;
}
/* Push Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .push_1 {
left:80px;
}
.container_12 .push_2 {
left:160px;
}
.container_12 .push_3 {
left:240px;
}
.container_12 .push_4 {
left:320px;
}
.container_12 .push_5 {
left:400px;
}
.container_12 .push_6 {
left:480px;
}
.container_12 .push_7 {
left:560px;
}
.container_12 .push_8 {
left:640px;
}
.container_12 .push_9 {
left:720px;
}
.container_12 .push_10 {
left:800px;
}
.container_12 .push_11 {
left:880px;
}
/* Pull Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .pull_1 {
left:-80px;
}
.container_12 .pull_2 {
left:-160px;
}
.container_12 .pull_3 {
left:-240px;
}
.container_12 .pull_4 {
left:-320px;
}
.container_12 .pull_5 {
left:-400px;
}
.container_12 .pull_6 {
left:-480px;
}
.container_12 .pull_7 {
left:-560px;
}
.container_12 .pull_8 {
left:-640px;
}
.container_12 .pull_9 {
left:-720px;
}
.container_12 .pull_10 {
left:-800px;
}
.container_12 .pull_11 {
left:-880px;
}
/* `Clear Floated Elements
----------------------------------------------------------------------------------------------------*/
/* http://sonspring.com/journal/clearing-floats */
.clear {
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}
/* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */
.clearfix:before,
.clearfix:after {
content: '\0020';
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}
.clearfix:after {
clear: both;
}
/*
The following zoom:1 rule is specifically for IE6 + IE7.
Move to separate stylesheet if invalid CSS is a problem.
*/
.clearfix {
zoom: 1;
}

View File

@@ -0,0 +1,53 @@
/*
** Base layout:
** Grid layout + vertical sizing classes sized to match
*/
/* Grid layout based on (http://960.gs/) */
@import url(grid.css);
/* Vertical classes */
.grid_1, .vert_1,
.grid_2, .vert_2,
.grid_3, .vert_3,
.grid_4, .vert_4,
.grid_5, .vert_5,
.grid_6, .vert_6,
.grid_7, .vert_7,
.grid_8, .vert_8,
.grid_9, .vert_9,
.grid_10, .vert_10,
.grid_11, .vert_11,
.grid_12, .vert_12 {
margin-top: 10px;
margin-bottom: 10px;
}
.container_12 .vert_1 { height:60px; }
.container_12 .vert_2 { height:140px; }
.container_12 .vert_3 { height:220px; }
.container_12 .vert_4 { height:300px; }
.container_12 .vert_5 { height:380px; }
.container_12 .vert_6 { height:460px; }
.container_12 .vert_7 { height:540px; }
.container_12 .vert_8 { height:620px; }
.container_12 .vert_9 { height:700px; }
.container_12 .vert_10 { height:780px; }
.container_12 .vert_11 { height:860px; }
.container_12 .vert_12 { height:940px; }
/* Layout details */
p:first-child {
margin-top: 0;
}
/* Log block */
.block.log {
overflow: auto;
}

View File

@@ -0,0 +1,30 @@
/* Basic style & theme*/
body {
font-family: sans-serif;
}
p {
text-align: justify;
}
code {
background-color: #eee;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0 0.2em;
}
pre {
padding: 0.5em 1.5em;
background-color: #eee;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
/* Devevlopment helpers */
.debug_red { background-color: rgba(255,0,0,0.5); }
.debug_green { background-color: rgba(0,255,0,0.5); }
.debug_blue { background-color: rgba(0,0,255,0.5); }

326
eca/__init__.py Normal file
View File

@@ -0,0 +1,326 @@
import queue
import collections
import threading
import logging
import sys
import json
from contextlib import contextmanager
from . import util
from . import pubsub
logger = logging.getLogger(__name__)
# all exported names
__all__ = [
'event',
'condition',
'rules',
'Rules',
'Context',
'Event',
'fire',
'fire_global',
'emit',
'get_context',
'spawn_context',
'context_activate',
'context_switch',
'auxiliary',
'register_auxiliary',
'shutdown'
]
# The global event channel
global_channel = pubsub.PubSubChannel()
# The thread local storage used to create a 'current context' with regards
# to the executing thread.
# (See https://docs.python.org/3/library/threading.html#thread-local-data)
thread_local = threading.local()
class Rules:
def __init__(self):
self.rules = set()
def prepare_action(self, fn):
"""
Prepares a function to be usable as an action.
This function assigns an empty list of the 'conditions' attribute if it is
not yet available. This function also registers the action with the action
library.
"""
if not hasattr(fn, 'conditions'):
logger.info("Defined action '{}'".format(fn.__name__))
fn.conditions = getattr(fn, 'conditions', [])
fn.events = getattr(fn, 'events', set())
self.rules.add(fn)
def condition(self, c):
"""
Adds a condition callable to the action.
The condition must be callable. The condition will receive a context and
an event, and must return True or False.
This function returns a decorator so we can pass an argument to the
decorator itself. This is why we define a new function and return it
without calling it.
(See http://docs.python.org/3/glossary.html#term-decorator)
"""
def condition_decorator(fn):
self.prepare_action(fn)
logger.debug("With condition: {}".format(util.describe_function(c)))
fn.conditions.append(c)
return fn
return condition_decorator
def event(self, eventname):
"""
Attaches the action to an event.
This is effectively the same as adding the 'event.name == eventname'
condition. Adding multiple event names will prevent the rule from
triggering.
As condition, this function generates a decorator.
"""
def event_decorator(fn):
self.prepare_action(fn)
logger.debug("Attached to event: {}".format(eventname))
fn.events.add(eventname)
return fn
return event_decorator
# The 'global' rules set
rules = Rules()
event = rules.event
condition = rules.condition
class Event:
"""Abstract event with a name and attributes."""
def __init__(self, name, data=None):
"""Constructs an event.
Attributes are optional.
"""
self.name = name
self.data = data
def get(self, *args, **kwargs):
return self.data.get(*args, **kwargs)
def __str__(self):
data_strings = []
if isinstance(self.data, collections.abc.Mapping):
for k, v in self.data.items():
data_strings.append("{}={}".format(k, v))
else:
data_strings.append(str(self.data))
return "'{}' with {{{}}}".format(self.name, ', '.join(data_strings))
class Context:
"""
ECA Execution context to track scope and events.
Each context maintains both a variables namespace and an event queue. The
context itself provides a run method to allow threaded execution through
starting a new thread targetted at the run method.
Every context also contains a dictionary of auxiliaries which contains
objects to support the context and its rule execution.
"""
def __init__(self, init_data=None, name='<unnamed context>', rules=rules):
self.event_queue = queue.Queue()
self.scope = util.NamespaceDict()
self.channel = pubsub.PubSubChannel()
self.auxiliaries = {}
self.name = name
self.done = False
self.daemon = True
self.rules = rules
# subscribe to own pubsub channel to receive events
self.channel.subscribe(self._pubsub_receiver, 'event')
self.receive_event(Event('init', init_data))
# subscribe to global pubsub channel to receive global eca events
global_channel.subscribe(self._pubsub_receiver, 'event')
def _trace(self, message):
"""Prints tracing statements if trace is enabled."""
logging.getLogger('trace').info(message)
def _pubsub_receiver(self, name, data):
"""Pubsub channel connector."""
self.receive_event(data)
def receive_event(self, event):
"""Receives an Event to handle."""
self._trace("Received event: {}".format(event))
self.event_queue.put(event)
def auxiliary(self, name):
return self.auxiliaries[name]
def run(self):
"""Main event loop."""
# switch context to this one and start working
with context_switch(self):
while not self.done:
self._handle_event()
def start(self, daemon=True):
thread = threading.Thread(target=self.run)
self.daemon = daemon
thread.daemon = self.daemon
thread.start()
def stop(self):
global_channel.unsubscribe(self._pubsub_receiver, 'event')
if not self.daemon:
self.done = True
else:
logger.warning("Can't shutdown daemon context. The context is used in a server.")
def _handle_event(self):
"""Handles a single event, or times out after receiving nothing."""
try:
# wait until we have an upcoming event
# (but don't wait too long -- self.done could have been set to
# true while we were waiting for an event)
event = self.event_queue.get(timeout=1.0)
self._trace("Working on event: {}".format(event))
# Determine candidate rules and execute matches:
# 1) Only rules that match the event name as one of the events
candidates = [r for r in self.rules.rules if event.name in r.events]
# 2) Only rules for which all conditions hold
for r in candidates:
if not [c(self.scope, event) for c in r.conditions].count(False):
self._trace("Rule: {}".format(util.describe_function(r)))
result = r(self.scope, event)
except queue.Empty:
# Timeout on waiting
pass
@contextmanager
def context_switch(context):
"""
Context manager to allow ad-hoc context switches. (The Python 'context' is
different from the eca Context.)
This function can be written without any regard for locking as the
thread_local object will take care of that. Since everything here is done
in the same thread, this effectively allows nesting of context switches.
"""
# activate new context and store old
old_context = context_activate(context)
yield
# restore old context
context_activate(old_context)
def context_activate(context):
"""
Activate an eca Context. If None is passed, this function should
disable the context.
"""
# stash old context
old_context = getattr(thread_local, 'context', None)
# switch to new context
thread_local.context = context
return old_context
def get_context():
"""Returns the current context."""
return getattr(thread_local, 'context', None)
def auxiliary(name):
"""
Returns an auxiliary for this context.
"""
context = get_context()
if context is None:
raise NotImplementedError("Can not get an auxiliary without a current context.")
return context.auxiliaries[name]
def register_auxiliary(name, aux):
"""
Registers an auxiliary object for this context.
"""
context = get_context()
if context is None:
raise NotImplementedError("Can not get an auxiliary without a current context.")
context.auxiliaries[name] = aux
def shutdown():
context = get_context()
if context is None:
raise NotImplementedError("Can not invoke shutdown without a current context.")
context.stop()
def fire(eventname, data=None, delay=None):
"""
Fires an event.
This is the fire-and-forget method to create new events.
"""
e = Event(eventname, data)
context = get_context()
if context is None:
raise NotImplementedError("Can't invoke fire without a current context.")
context.channel.publish('event', e, delay)
def fire_global(eventname, data=None, delay=None):
"""
Fires a global event.
"""
e = Event(eventname, data)
global_channel.publish('event', e, delay)
def emit(name, data, id=None):
"""
Emits an event to whomever is listening (mostly HTTP clients).
"""
e = Event(name, {
'json': json.dumps(data),
'id': id
})
context = get_context()
if context is None:
raise NotImplementedError("Can't invoke emit without a current context.")
context.channel.publish('emit', e)
def spawn_context(init_data=None, name='<unnamed context>', rules=rules, daemon=False):
"""
Spawns a new context and starts it.
"""
context = Context(init_data, name, rules)
context.start(daemon)

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

384
eca/arff.py Normal file
View File

@@ -0,0 +1,384 @@
"""
ARFF format loading and saving module.
This module implements the book version [1] of the ARFF format. This means there
is no support for instance weights.
Known limitations:
- This implementation does not parse dates
[1]: http://weka.wikispaces.com/ARFF+%28book+version%29
"""
import re
from collections import namedtuple
Field = namedtuple('Field',['name','type'])
__all__ = ['load', 'save', 'Field', 'Numeric', 'Text', 'Nominal']
#
# Line type functions
#
def is_empty(line):
return not line.strip()
def is_comment(line):
return line.startswith('%')
def format_comment(line):
return '% '+line
def is_relation(line):
return line.lower().startswith('@relation')
def format_relation(name):
return '@relation ' + format_identifier(name) + '\n'
def is_attribute(line):
return line.lower().startswith('@attribute')
def format_attribute(field):
return '@attribute ' + format_identifier(field.name) + ' ' + str(field.type) + '\n'
def format_attributes(fields):
result = []
for field in fields:
result.append(format_attribute(field))
return ''.join(result)
def is_data(line):
return line.lower().startswith('@data')
def format_data():
return '@data\n'
def format_row(row, fields, sparse=False):
"""Formats a data row based on the given fields."""
if sparse:
result = []
for i in range(len(fields)):
field = fields[i]
val = row.get(field.name)
if val != field.type.default():
result.append(format_numeric(i) + ' ' + field.type.format(val))
return '{' + ','.join(result) + '}\n'
else:
result = []
for field in fields:
result.append(field.type.format(row.get(field.name)))
return ','.join(result)+'\n'
def safe_next(it):
"""Returns the next character from the iterator or ''."""
try:
return next(it)
except StopIteration:
return ''
def whitespace(rest):
"""Parses whitespace at the beginning of the input."""
return rest.lstrip()
number_pattern = re.compile(r'[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?')
def numeric(rest):
"""Parses a number at the beginning of the input."""
m = number_pattern.match(rest)
if m:
rest = rest[len(m.group(0)):]
try:
number = int(m.group(0))
except ValueError:
number = float(m.group(0))
return number, rest
else:
raise ValueError('Number not parsable')
def format_numeric(number):
"""Outputs a number."""
return str(number)
def expect(rest, string):
"""Expects to see the string at the start of the input."""
result = rest.startswith(string)
if result:
return result, rest[len(string):]
else:
return False, rest
identifier_escapes = {
'\\': '\\',
'n' : '\n',
't' : '\t',
'r' : '\r',
'%' : '%',
"'" : "'"
}
def identifier(rest):
"""Parses an optionally quoted identifier at the start of the input."""
name = ''
it = iter(rest)
c = safe_next(it)
# non-quoted
if c != "'":
while c and c not in [' ', '\t', ',']:
name += c
c = safe_next(it)
return name, c + ''.join(it)
# quoted
# discard the opening quote by fetching next character
c = safe_next(it)
while c:
if c == '\\':
ec = safe_next(it)
if not ec:
raise ValueError('Input end during escape.')
try:
name += identifier_escapes[ec]
except KeyError:
name += '\\' + ec
elif c == "'":
break
else:
name += c
c = safe_next(it)
return name, ''.join(it)
def format_identifier(name):
"""Formats an identifier."""
reverse_escapes = { c:ec for (ec,c) in identifier_escapes.items()}
if any(x in name for x in [' ',','] + list(reverse_escapes.keys())):
escaped = ''
for c in name:
if c in reverse_escapes:
escaped += '\\' + reverse_escapes[c]
else:
escaped += c
return "'"+escaped+"'"
return name
class Numeric:
"""Numeric field type."""
def parse(self, rest):
if rest.startswith('?'):
return None, rest[1:]
return numeric(rest)
def format(self, number):
if number is None:
return '?'
else:
return format_numeric(number)
def default(self):
return 0
def __repr__(self):
return 'Numeric'
def __str__(self):
return 'numeric'
class Text:
"""Text field type."""
def parse(self, rest):
if rest.startswith('?'):
return None, rest[1:]
return identifier(rest)
def format(self, name):
if name is None:
return '?'
else:
return format_identifier(name)
def default(self):
return ''
def __repr__(self):
return 'Text'
def __str__(self):
return 'string'
class Nominal:
"""Nominal field type."""
def __init__(self, names):
self.values = names
def parse(self, rest):
if rest.startswith('?'):
return None, rest[1:]
name, rest = identifier(rest)
if name in self.values:
return name, rest
else:
raise ValueError('Unknown nominal constant "{}" for {}.'.format(name, self.values))
def format(self, name):
if name is None:
return '?'
else:
if name not in self.values:
raise ValueError('Unknown nominal constant "{}" for {}.'.format(name, self.values))
return format_identifier(name)
def default(self):
return self.values[0]
def __repr__(self):
return 'Nominal in {}'.format(self.values)
def __str__(self):
return '{' + ', '.join(format_identifier(name) for name in self.values) + '}'
def attr_type(rest):
"""Parses a field type. Uses the whole rest."""
if rest.lower() in ['numeric', 'integer', 'real']:
return Numeric()
elif rest.lower() in ['string']:
return Text()
elif rest.lower().startswith('date'):
raise NotImplementedError('date parsing is not implemented.')
elif rest.startswith('{') and rest.endswith('}'):
names = []
rest = rest[1:-1]
while rest:
rest = whitespace(rest)
name, rest = identifier(rest)
names.append(name)
rest = whitespace(rest)
seen, rest = expect(rest, ',')
if not seen:
break
return Nominal(names)
else:
raise ValueError('Unknown attribute type "{}"'.format(rest))
def parse_attribute(line):
"""Parses an attribute line."""
# @attribute WS name WS type
rest = line[len('@attribute'):].strip()
rest = whitespace(rest)
name, rest = identifier(rest)
rest = whitespace(rest)
type = attr_type(rest)
return name, type
def parse_row(line, fields):
"""Parses a row. Row can be normal or sparse."""
line = line.strip()
values = {}
if not line.startswith('{'):
rest = line
first = True
for field in fields:
if not first:
rest = whitespace(rest)
seen, rest = expect(rest, ',')
first = False
rest = whitespace(rest)
value, rest = field.type.parse(rest)
values[field.name] = value
return values
else:
todo = set(range(len(fields)))
rest = line[1:-1].strip()
first = True
while rest:
if not first:
rest = whitespace(rest)
seen, rest = expect(rest, ',')
if not seen:
break
first = False
rest = whitespace(rest)
index, rest = numeric(rest)
field = fields[index]
rest = whitespace(rest)
value, rest = field.type.parse(rest)
todo.remove(index)
values[field.name] = value
for field in (fields[i] for i in todo):
values[field.name] = field.type.default()
return values
def load(fileish):
"""
Loads a data set from an arff formatted file-like object.
This generator function will parse the arff format's header to determine
data shape. Each generated item is a single expanded row.
fileish -- a file-like object
"""
# parse header first
lines = iter(fileish)
fields = []
for line in lines:
if is_empty(line) or is_comment(line):
continue
if is_relation(line):
# No care is given for the relation name.
continue
if is_attribute(line):
name, type = parse_attribute(line)
fields.append(Field(name, type))
continue
if is_data(line):
# We are done with the header, next up is 1 row per line
break
# parse data lines
for line in lines:
if is_empty(line) or is_comment(line):
continue
row = parse_row(line, fields)
yield row
def save(fileish, fields, rows, name='unnamed relation', sparse=False):
"""
Saves an arff formatted data set to a file-like object.
The rows parameter can be any iterable. The fields parameter must be a list
of `Field` instances.
fileish -- a file-like object to write to
fields -- a list of `Field` instances
rows -- an iterable containing one dictionary per data row
name -- the relation name, defaults to 'unnamed relation'
sparse -- whether the output should be in sparse format, defaults to False
"""
fileish.write(format_relation(name))
fileish.write('\n')
fileish.write(format_attributes(fields))
fileish.write('\n')
fileish.write(format_data())
for row in rows:
fileish.write(format_row(row, fields, sparse))

120
eca/generators.py Normal file
View File

@@ -0,0 +1,120 @@
import threading
import time
from datetime import datetime
import json
from . import fire, get_context, context_switch, register_auxiliary, auxiliary
from . import arff
import logging
import sys
logger = logging.getLogger(__name__)
class EventGenerator:
"""
An event generator uses a generation function to generate events from
any external source.
"""
def __init__(self, context, generator, event_name='tweet', **kwargs):
self.context = context
self.event_name = event_name
self.generator = generator
self.generator_args = kwargs
self.stop_flag = threading.Event()
def start(self):
"""
Starts a thread to handle run this generator.
"""
thread = threading.Thread(target=self.run)
thread.start()
def stop(self):
"""
Requests shutdown of generator.
"""
self.stop_flag.set()
def run(self):
"""
Invoke the generator to get a sequence of events.
This method passes an event to the generator which will be set to True
if the generator should terminate. Immediate termination is not required.
"""
logger.debug("Running event generator")
with context_switch(self.context):
for event in self.generator(self.stop_flag, **self.generator_args):
fire(self.event_name, event)
def offline_tweets(stop, data_file, time_factor=1000, arff_file=None):
"""
Offline tweet replay.
Takes a datafile formatted with 1 tweet per line, and generates a sequence of
scaled realtime items.
"""
# timing functions return false if we need to abort
def delayer(duration):
logger.debug("Delay for next tweet {}s ({}s real)".format(delay, delay/time_factor))
return not stop.wait(delay / time_factor)
def immediate(duration):
return not stop.is_set()
# select timing function based on time_factor
delayed = immediate if time_factor is None else delayer
arff_data = None
if arff_file:
arff_file = open(arff_file, 'r', encoding='utf-8')
arff_data = arff.load(arff_file)
with open(data_file, encoding='utf-8') as data:
last_time = None
lines = 0
for line in data:
lines += 1
try:
tweet = json.loads(line)
if arff_file:
try:
extra_data = next(arff_data)
except StopIteration:
extra_data = None
except ValueError as e:
logger.error("Could not read arff line for tweet (reason: {})".format(e))
extra_data = None
tweet['extra'] = extra_data
except ValueError as e:
logger.error("Could not read tweet on {}:{} (reason: {})".format(data_file,lines, e))
continue
# time scale the tweet
tweet_time = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
if not last_time:
last_time = tweet_time
wait = tweet_time - last_time
delay = wait.total_seconds()
# delay and yield or break depending on success
if delayed(delay):
yield tweet
last_time = tweet_time
else:
break
if arff_file:
arff_file.close()
def start_offline_tweets(data_file, event_name='tweet', aux_name='tweeter', **kwargs):
context = get_context()
if context is None:
raise NotImplementedError("Can not start offline tweet replay outside of a context.")
register_auxiliary(aux_name, EventGenerator(context, generator=offline_tweets, data_file=data_file, event_name=event_name, **kwargs))
auxiliary(aux_name).start()

135
eca/http.py Normal file
View File

@@ -0,0 +1,135 @@
import http.cookies
import logging
import json
import collections
from . import httpd
from . import sse
from . import fire, get_context
from . import sessions
# Logging
logger = logging.getLogger(__name__)
# bring some external handlers into this module's scope
# (now you can just import eca.http and have access to all standard handlers)
StaticContent = httpd.StaticContent
SessionManager = sessions.SessionManager
class Cookies(httpd.Filter):
"""Filter to read cookies from request."""
def handle(self):
# process available cookies
cookies = http.cookies.SimpleCookie()
if 'cookie' in self.request.headers:
cookies.load(self.request.headers['cookie'])
# set cookies on request
self.request.cookies = cookies
class HelloWorld(httpd.Handler):
"""The mandatory Hello World example."""
def handle_GET(self):
self.request.send_response(200)
self.request.send_header('content-type','text/html; charset=utf-8')
self.request.end_headers()
output = "<!DOCTYPE html><html><body><h1>Hello world!</h1><p><i>eca-session:</i> {}</p></body></html>"
try:
if not hasattr(self.request, 'cookies'): raise KeyError()
cookie = self.request.cookies['eca-session'].value
except KeyError:
cookie = '<i>no cookie</i>';
self.request.wfile.write(output.format(cookie).encode('utf-8'))
def Redirect(realpath):
"""
Factory for redirection handlers.
"""
class RedirectHandler(httpd.Handler):
def handle_GET(self):
location = None
# check for absolute paths
if realpath.startswith("http://") or realpath.startswith('https://'):
location = realpath
else:
host = self.request.server.server_address[0]
if self.request.server.server_address[1] != 80:
host += ":{}".format(self.request.server.server_address[1])
if 'host' in self.request.headers:
host = self.request.headers['host']
location = "http://{}{}".format(host, realpath)
self.request.send_response(302)
self.request.send_header('content-type','text/html; charset=utf-8')
self.request.send_header('location',location)
self.request.end_headers()
output = "<!DOCTYPE html><html><body><p>Redirect to <a href='{0}'>{0}</a></p></body></html>"
self.request.wfile.write(output.format(location).encode('utf-8'))
return RedirectHandler
def GenerateEvent(name):
"""
This function returns a handler class that creates the named event based
on the posted JSON data.
"""
class EventGenerationHandler(httpd.Handler):
def handle_POST(self):
# handle weirdness
if 'content-length' not in self.request.headers:
self.request.send_error(411)
return
# read content-length header
length = int(self.request.headers['content-length'])
# grab data
data = self.request.rfile.read(length)
try:
structured = json.loads(data.decode('utf-8'))
except ValueError as e:
self.request.send_error(400, "Bad request: "+str(e))
return
if not isinstance(structured, collections.abc.Mapping):
self.request.send_error(400, "Bad request: expect a JSON object")
return
try:
fire(name, structured)
except NotImplementedError:
logger.warn("Event generated by HTTP request without active session. Do you have a SessionManager configured?")
self.request.send_error(500, "No current context available.")
return
self.request.send_response(202)
self.request.send_header('content-type', 'text/plain; charset=utf-8')
self.request.send_header('content-length', 0)
self.request.end_headers()
return EventGenerationHandler
class EventStream(sse.ServerSideEvents):
def go_subscribe(self):
def receiver(name, event):
self.send_event(event.data.get('json'), event.name, event.data.get('id'))
self.receiver = receiver
context = get_context()
context.channel.subscribe(self.receiver, 'emit')
def go_unsubscribe(self):
context = get_context()
context.channel.unsubscribe(self.receiver, 'emit')

326
eca/httpd.py Normal file
View File

@@ -0,0 +1,326 @@
import http.server
import http.cookies
import socketserver
import logging
import os.path
import posixpath
import urllib
from collections import namedtuple
# Logging
logger = logging.getLogger(__name__)
# Hard-coded default error message
DEFAULT_ERROR_MESSAGE = """\
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
* { /* Reset the worst style breakers */
padding: 0;
margin: 0;
}
html { /* We always want at least this height */
min-height: 100%%;
}
body#error {
font-family: sans-serif;
height: 100%%;
background: #3378c6;
background: -webkit-radial-gradient(center, ellipse cover, #3378c6 0%%,#23538a 100%%);
background: radial-gradient(ellipse at center, #3378c6 0%%,#23538a 100%%);
background-size: 100%% 100%%;
background-repeat: no-repeat;
}
#error #message {
position: absolute;
width: 34em;
height: 4em;
top: 50%%;
margin-top: -2em;
left: 50%%;
margin-left: -17em;
text-align: center;
color: #114;
text-shadow: 0 1px 0 #88d;
}
</style>
<title>%(code)d - %(message)s</title>
</head>
<body id='error'>
<div id='message'>
<h1>%(message)s</h1>
<span>%(explain)s</span>
</div>
</body>
</html>
"""
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""
This request handler routes requests to a specialised handler.
Handling a request is roughly done in two steps:
1) Requests are first passed through matching registered filters
2) Request is passed to the matching handler.
Responsibility for selecting the handler is left to the server class.
"""
error_message_format = DEFAULT_ERROR_MESSAGE
server_version = 'EcaHTTP/2'
default_request_version = 'HTTP/1.1'
def send_header(self, key, value):
"""Buffer headers until they can be sent."""
if not self.response_sent:
if not hasattr(self, '_cached_headers'):
self._cached_headers = []
self._cached_headers.append((key,value))
else:
super().send_header(key, value)
def send_response(self, *args, **kwargs):
"""Sends the necessary response, and appends buffered headers."""
super().send_response(*args, **kwargs)
self.response_sent = True
if hasattr(self, '_cached_headers'):
for h in self._cached_headers:
self.send_header(*h)
self._cached_headers = []
def dispatch(self):
"""Dispatch incoming requests."""
self.handler = None
self.response_sent = False
# the method we will be looking for
# (uses HTTP method name to build Python method name)
method_name = "handle_{}".format(self.command)
# let server determine specialised handler factory, and call it
handler_factory = self.server.get_handler(self.command, self.path)
if not handler_factory:
self.send_error(404)
return
# instantiate handler
self.handler = handler_factory(self)
# check for necessary HTTP method
if not hasattr(self.handler, method_name):
self.send_error(501, "Unsupported method ({})".format(self.command))
return
# apply filters to request
# note: filters are applied in order of registration
for filter_factory in self.server.get_filters(self.command, self.path):
filter = filter_factory(self)
if not hasattr(filter, method_name):
self.send_error(501, "Unsupported method ({})".format(self.command))
return
filter_method = getattr(filter, method_name)
filter_method()
# select and invoke actual method
method = getattr(self.handler, method_name)
method()
def translate_path(self, path):
"""
Translate a /-separated PATH to the local filename syntax.
This method is unelegantly 'borrowed' from SimpleHTTPServer.py to change
the original so that it has the `path = self.server.static_path' line.
"""
# abandon query parameters
path = path.split('?',1)[0]
path = path.split('#',1)[0]
path = path[len(self.url_path):]
# Don't forget explicit trailing slash when normalizing. Issue17324
trailing_slash = path.rstrip().endswith('/')
path = posixpath.normpath(urllib.parse.unquote(path))
words = path.split('/')
words = filter(None, words)
# server content from static_path, instead of os.getcwd()
path = self.local_path
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir): continue
path = os.path.join(path, word)
if trailing_slash:
path += '/'
return path
# Standard HTTP verbs bound to dispatch method
def do_GET(self): self.dispatch()
def do_POST(self): self.dispatch()
def do_PUT(self): self.dispatch()
def do_DELETE(self): self.dispatch()
def do_HEAD(self): self.dispatch()
# Fallback handlers for static content
# (These invoke the original SimpleHTTPRequestHandler behaviour)
def handle_GET(self): super().do_GET()
def handle_HEAD(self): super().do_HEAD()
# handle logging
def _log_data(self):
path = getattr(self, 'path','<unknown path>')
command = getattr(self, 'command', '<unknown command>')
return {
'address': self.client_address,
'location': path,
'method': command
}
def _get_message_format(self, format, args):
log_data = self._log_data()
message_format = "[{}, {} {}] {}".format(self.client_address[0],
log_data['method'],
log_data['location'],
format%args)
return message_format
#overload logging methods
def log_message(self, format, *args):
logger.debug(self._get_message_format(format, args), extra=self._log_data())
def log_error(self, format, *args):
logger.warn(self._get_message_format(format, args), extra=self._log_data())
HandlerRegistration = namedtuple('HandlerRegistration',['methods','path','handler'])
class HTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
"""
HTTP Server with path/method registration functionality to allow simple
configuration of served content.
"""
def __init__(self, server_address, RequestHandlerClass=HTTPRequestHandler):
self.handlers = []
self.filters = []
super().__init__(server_address, RequestHandlerClass)
def get_handler(self, method, path):
"""Selects the best matching handler."""
# Select handlers for the given method, that match any path or a prefix of the given path
matches = [m
for m
in self.handlers
if (not m.methods or method in m.methods) and path.startswith(m.path)]
# if there are matches, we select the one with the longest matching prefix
if matches:
best = max(matches, key=lambda e: len(e.path))
return best.handler
else:
return None
def get_filters(self, method, path):
"""Selects all applicable filters."""
# Select all filters that the given method, that match any path or a suffix of the given path
return [f.handler
for f
in self.filters
if (not f.methods or method in f.methods) and path.startswith(f.path)]
def _log_registration(self, kind, registration):
message_format = "Adding HTTP request {} {} for ({} {})"
message = message_format.format(kind,
registration.handler,
registration.methods,
registration.path)
logger.info(message)
def add_route(self, path, handler_factory, methods=["GET"]):
"""
Adds a request handler to the server.
The handler can be specialised in in or more request methods by
providing a comma separated list of methods. Handlers are matched
longest-matching-prefix with regards to paths.
"""
reg = HandlerRegistration(methods, path, handler_factory)
self._log_registration('handler', reg)
self.handlers.append(reg)
def add_content(self, path, local_path, methods=['GET','HEAD']):
"""
Adds a StaticContent handler to the server.
This method is shorthand for
self.add_route(path, StaticContent(path, local_path), methods)
"""
if not path.endswith('/'):
logger.warn("Static content configured without trailing '/'. "+
"This is different from traditional behaviour.")
logger.info("Serving static content for {} under '{}' from '{}'".format(methods,path,local_path))
self.add_route(path, StaticContent(path, local_path), methods)
def add_filter(self, path, filter_factory, methods=[]):
"""
Adds a filter to the server.
Like handlers, filters can be specialised on in or more request methods
by providing a comma-separated list of methods. Filters are selected on
match prefix with regards to paths.
Filters are applied in order of registration.
"""
reg = HandlerRegistration(methods, path, filter_factory)
self._log_registration('filter', reg)
self.filters.append(reg)
def serve_forever(self):
logger.info("Server is running...")
super().serve_forever()
class Handler:
"""
Handler base class.
"""
def __init__(self, request):
self.request = request
class Filter(Handler):
"""
Filter base class that does nothing.
"""
def handle_GET(self): self.handle()
def handle_POST(self): self.handle()
def handle_HEAD(self): self.handle()
def handle(self):
pass
# static content handler defined here, because of intrinsice coupling with
# request handler.
def StaticContent(url_path, local_path):
class StaticContent(Handler):
"""
Explicit fallback handler.
"""
def set_paths(self):
self.request.local_path = local_path
self.request.url_path = url_path
def handle_GET(self):
self.set_paths()
self.request.handle_GET()
def handle_HEAD(self):
self.set_paths()
self.request.handle_HEAD()
# return class so that it can be constructed
return StaticContent

50
eca/pubsub.py Normal file
View File

@@ -0,0 +1,50 @@
import collections
import threading
class PubSubChannel:
"""
Publish/Subscribe channel used for distribution of events.
The operations on this channel are thread-safe, but subscribers
are executed by the publishing thread. Use a queue to decouple the
publishing thread from the consuming thread.
"""
def __init__(self):
self.lock = threading.RLock()
self.subscriptions = collections.defaultdict(list)
def subscribe(self, target, event='message'):
"""
Subscribe to an event.
The optional event name can be used to subscribe selectively.
"""
with self.lock:
self.subscriptions[event].append(target)
def unsubscribe(self, target, event='message'):
"""
Unsubscribe from an event.
The optional event name can be used to unsubscribe from another event.
"""
with self.lock:
self.subscriptions[event].remove(target)
def publish(self, event='message', data=None, delay=None):
"""
Publishes an event.
The event can be accompanied by optional data. A delay can be set to
delay the publish action by the given amount of seconds.
"""
if delay is None:
with self.lock:
for target in self.subscriptions[event]:
target(event, data)
else:
def task():
self.publish(event, data)
threading.Timer(delay, task).start()

128
eca/sessions.py Normal file
View File

@@ -0,0 +1,128 @@
from http.cookies import SimpleCookie
from collections import namedtuple
from collections.abc import Mapping
from itertools import product, chain
import time
import random
from . import httpd
from . import Context, context_activate
# Name generation for contexts and sessions
def name_parts():
"""
This generator will create an endless list of steadily increasing
name part lists.
"""
# name parts
letters = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron',
'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi',
'omega']
colours = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']
# randomize order
random.shuffle(letters)
random.shuffle(colours)
# yield initial sequence (letter-colour)
parts = [letters, colours]
yield parts
# forever generate longer sequences by appending the letter list
# over and over. Note that this is the *same* letter list, so it will have
# the exact order.
while True:
random.shuffle(letters)
random.shuffle(colours)
parts.append(letters)
yield parts
# construct an iterator that will endlessly generate names:
# 1) for each parts list p in name_parts() we take the cartesian product
# 2) the product iterators are generated by the for...in generator
# 3) we chain these iterators so that when the first is exhausted, we can
# continue with the second, etc.
# 4) we map the function '-'.join over the list of parts from the chain
names = map('-'.join, chain.from_iterable((product(*p) for p in name_parts())))
class SessionCookie(httpd.Filter):
"""
The actual HTTP filter that will apply the cookie handling logic to each
request. This filter defers to the SessionManager with respect to the
cookie name to use and the activation of sessions.
"""
def bind(self, manager):
"""Post constructor configuration of filter."""
self.manager = manager
def handle(self):
"""
Determine if a cookie needs to be set and let the session manager
handle activation.
"""
cookies = self.request.cookies
morsel = cookies.get(self.manager.cookie)
if not morsel:
# Determine new cookie
value = self.manager.generate_name()
# Set new cookie
cookies[self.manager.cookie] = value
cookies[self.manager.cookie]['path'] = '/'
# Send the new cookie as header
self.request.send_header('Set-Cookie', cookies[self.manager.cookie].OutputString())
else:
value = morsel.value
self.manager.activate(value)
class Session:
"""
The Session bookkeeping data.
"""
def __init__(self, context, seen):
self.context = context
self.seen = seen
def activate(self):
"""Activate the session. Updates last seen time."""
self.seen = time.time()
context_activate(self.context)
class SessionManager:
"""
The SessionManager class. This class is callable so it can be used in place
of a constructor in the configuration.
"""
def __init__(self, cookie_name):
self.sessions = {}
self.cookie = cookie_name
def __call__(self, *args, **kwargs):
handler = SessionCookie(*args, **kwargs)
handler.bind(self)
return handler
def generate_name(self):
result = next(names)
while result in self.sessions:
result = next(names)
return result
def _new_session(self, name):
result = Session(Context(name=name, init_data={'name': name}), time.time())
result.context.start()
return result
def activate(self, name):
if name not in self.sessions:
self.sessions[name] = self._new_session(name)
self.sessions[name].activate()

67
eca/sse.py Normal file
View File

@@ -0,0 +1,67 @@
import queue
from collections import namedtuple
from . import httpd
PendingEvent = namedtuple('PendingEvent', ['data', 'name', 'id'])
class ServerSideEvents(httpd.Handler):
"""
Base class for server side events. See the specification of the W3C
at http://dev.w3.org/html5/eventsource/
This class handles decoupling through the default Queue. Events can be
posted for transmission by using send_event.
"""
def __init__(self, request):
super().__init__(request)
self.queue = queue.Queue()
def send_event(self, data, name=None, id=None):
self.queue.put(PendingEvent(data, name, id))
def go_subscribe(self):
pass
def go_unsubscribe(self):
pass
def handle_GET(self):
self.go_subscribe()
# Send HTTP headers:
self.request.send_response(200)
self.request.send_header("Content-type", "text/event-stream")
self.request.end_headers()
done = False
while not done:
event = self.queue.get()
if event == None:
done = True
else:
done = not self._send_message(event)
self.go_unsubscribe()
def _send_message(self, event):
try:
if event.id is not None:
id_line = "id: {}\n".format(event.id)
self.request.wfile.write(id_line.encode('utf-8'))
if event.name is not None:
event_line = "event: {}\n".format(event.name)
self.request.wfile.write(event_line.encode('utf-8'))
data_line = "data: {}\n".format(event.data)
self.request.wfile.write(data_line.encode('utf-8'))
self.request.wfile.write("\n".encode('utf-8'))
self.request.wfile.flush()
return True
except IOError:
return False

39
eca/util.py Normal file
View File

@@ -0,0 +1,39 @@
import os.path
class NamespaceError(KeyError):
"""Exception raised for errors in the NamespaceDict."""
pass
class NamespaceDict(dict):
"""
A dictionary that also allows access through attributes.
See http://docs.python.org/3.3/reference/datamodel.html#customizing-attribute-access
"""
def __getattr__(self, name):
if name not in self:
raise NamespaceError(name)
return self[name]
def __setattr__(self, name, value):
self[name] = value
def __delattr__(self, name):
del self[name]
def describe_function(fn):
"""
Generates a human readable reference to the given function.
This function is most useful when used on function defined in actual files.
"""
parts = []
parts.append(fn.__name__)
parts.append(" ({}:{})".format(os.path.relpath(fn.__code__.co_filename), fn.__code__.co_firstlineno))
return ''.join(parts)

150
neca.py Executable file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
import argparse
import threading
import importlib
import os.path
import sys
import logging
import json
from eca import *
import eca.httpd
import eca.http
# logging
logger = logging.getLogger(__name__)
def _hr_items(seq):
"""Creates a more readable comma-separated list of things."""
return ', '.join("'{}'".format(e) for e in seq)
def log_level(level):
"""argparse type to allow log level to be set directly."""
numeric_level = getattr(logging, level.upper(), None)
if not isinstance(numeric_level, int):
message_template = "'{}' is not a valid logging level. Choose from {}"
message = message_template.format(level, _hr_items(log_level.allowed))
raise argparse.ArgumentTypeError(message)
return numeric_level
# the following are allowed names for log levels
log_level.allowed = ['debug', 'info', 'warning', 'error', 'critical']
def main_server(args, rules_module):
"""HTTP server entry point."""
# determine initial static content path
rules_path = os.path.dirname(os.path.abspath(rules_module.__file__))
rules_file = os.path.basename(os.path.abspath(rules_module.__file__))
rules_file, rules_ext = os.path.splitext(rules_file)
root_path = os.path.join(rules_path, "{}_static".format(rules_file))
# see if an override has been given (absolute or relative)
if hasattr(rules_module, 'root_content_path'):
if os.path.isabs(rules_module.root_content_path):
root_path = rules_module.root_content_path
else:
root_path = os.path.join(rules_path, rules_module.root_content_path)
# configure http server
httpd = eca.httpd.HTTPServer((args.ip, args.port))
# default root route
httpd.add_content('/', root_path)
# default events route
httpd.add_route('/events', eca.http.EventStream)
# default handlers for cookies and sessions
httpd.add_filter('/', eca.http.Cookies)
httpd.add_filter('/', eca.http.SessionManager('eca-session'))
# invoke module specific configuration
if hasattr(rules_module, 'add_request_handlers'):
rules_module.add_request_handlers(httpd)
# start serving
httpd.serve_forever()
def main_engine(args, rules_module):
"""
Rules engine only entry point.
"""
# create context
context = Context(init_data={'name': '__main__'})
context.start(daemon=False)
# attach printing emit listener to context
def emitter(name, event):
print("emit '{}': {}".format(event.name, json.loads(event.get('json'))))
context.channel.subscribe(emitter, 'emit')
# fire main event
with context_switch(context):
logger.info("Starting module '{}'...".format(args.file))
fire('main')
# then read each line and process
for line in sys.stdin:
fire('line', line)
fire('end-of-input')
def main():
"""
Main program entry point.
"""
parser = argparse.ArgumentParser(description='The Neca HTTP server.')
parser.set_defaults(entry_point=main_engine)
parser.add_argument('file',
default='simple.py',
help="The rules file to load (defaults to %(default)s).",
nargs='?')
parser.add_argument('-t', '--trace',
default=False,
action='store_true',
help='Trace the execution of rules.')
parser.add_argument('-l', '--log',
default='warning',
help="The log level to use. One of {} (defaults to '%(default)s')".format(_hr_items(log_level.allowed)),
metavar='LEVEL',
type=log_level)
parser.add_argument('-s', '--server',
dest='entry_point',
action='store_const',
const=main_server,
help='Start HTTP server instead of directly executing the module.')
parser.add_argument('-p', '--port',
default=8080,
help="The port to bind the HTTP server to (default to '%(default)s')",
type=int)
parser.add_argument('-i', '--ip',
default='localhost',
help="The IP to bind the HTTP server to (defaults to '%(default)s'")
args = parser.parse_args()
# set logging level
logging.basicConfig(level=args.log)
# enable trace logger if requested
if args.trace:
logging.getLogger('trace').setLevel(logging.DEBUG)
# load module
rules_dir, rules_file = os.path.split(args.file)
rules_name = os.path.splitext(rules_file)[0]
old_path = list(sys.path)
sys.path.insert(0, rules_dir)
try:
rules_module = importlib.import_module(rules_name)
finally:
sys.path[:] = old_path
args.entry_point(args, rules_module)
if __name__ == "__main__":
main()

2036
p2000.json
View File

File diff suppressed because it is too large Load Diff

105
sports.json Normal file
View File

@@ -0,0 +1,105 @@
{
"created_at":"Sat Nov 16 12:51:41 +0000 2019",
"id":1195685871556710402,
"id_str":"1195685871556710402",
"text":"@BobGreenburg @ONeill_Coffee Congrats to the excellent football program at Wilmington. One classy organization!",
"display_text_range":[
29,
111
],
"source":"<a href=\"http:\/\/twitter.com\/download\/iphone\" rel=\"nofollow\">Twitter for iPhone<\/a>",
"truncated":false,
"in_reply_to_status_id":1195545633685475333,
"in_reply_to_status_id_str":"1195545633685475333",
"in_reply_to_user_id":483881032,
"in_reply_to_user_id_str":"483881032",
"in_reply_to_screen_name":"BobGreenburg",
"user":{
"id":774718832212606976,
"id_str":"774718832212606976",
"name":"Express Youngstown",
"screen_name":"ExpressProsYO",
"location":"5815 Market Street",
"url":"http:\/\/apply.expresspros.com\/",
"description":"Express Employment Professionals is one of the top staffing companies in the U.S. and Canada. WE want to help YOU succeed and find a job that you love!",
"translator_type":"none",
"protected":false,
"verified":false,
"followers_count":19,
"friends_count":136,
"listed_count":0,
"favourites_count":474,
"statuses_count":218,
"created_at":"Sat Sep 10 21:18:58 +0000 2016",
"utc_offset":null,
"time_zone":null,
"geo_enabled":false,
"lang":null,
"contributors_enabled":false,
"is_translator":false,
"profile_background_color":"F5F8FA",
"profile_background_image_url":"",
"profile_background_image_url_https":"",
"profile_background_tile":false,
"profile_link_color":"1DA1F2",
"profile_sidebar_border_color":"C0DEED",
"profile_sidebar_fill_color":"DDEEF6",
"profile_text_color":"333333",
"profile_use_background_image":true,
"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/775167844921188353\/fWquHsOK_normal.jpg",
"profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/775167844921188353\/fWquHsOK_normal.jpg",
"profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/774718832212606976\/1473649631",
"default_profile":true,
"default_profile_image":false,
"following":null,
"follow_request_sent":null,
"notifications":null
},
"geo":null,
"coordinates":null,
"place":null,
"contributors":null,
"is_quote_status":false,
"quote_count":0,
"reply_count":0,
"retweet_count":0,
"favorite_count":0,
"entities":{
"hashtags":[
],
"urls":[
],
"user_mentions":[
{
"screen_name":"BobGreenburg",
"name":"Bob Greenburg",
"id":483881032,
"id_str":"483881032",
"indices":[
0,
13
]
},
{
"screen_name":"ONeill_Coffee",
"name":"O'NeillCoffeeCompany",
"id":2804543925,
"id_str":"2804543925",
"indices":[
14,
28
]
}
],
"symbols":[
]
},
"favorited":false,
"retweeted":false,
"filter_level":"low",
"lang":"en",
"timestamp_ms":"1573908701205"
}

39
template.py Normal file
View File

@@ -0,0 +1,39 @@
from eca import *
import random
## You might have to update the root path to point to the correct path
## (by default, it points to <rules>_static)
# root_content_path = 'template_static'
# binds the 'setup' function as the action for the 'init' event
# the action will be called with the context and the event
@event('init')
def setup(ctx, e):
ctx.count = 0
fire('sample', {'previous': 0.0})
# define a normal Python function
def clip(lower, value, upper):
return max(lower, min(value, upper))
@event('sample')
def generate_sample(ctx, e):
ctx.count += 1
if ctx.count % 50 == 0:
emit('debug', {'text': 'Log message #'+str(ctx.count)+'!'})
# base sample on previous one
sample = clip(-100, e.data['previous'] + random.uniform(+5.0, -5.0), 100)
# emit to outside world
emit('sample',{
'action': 'add',
'value': sample
})
# chain event
fire('sample', {'previous': sample}, delay=0.05)

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Neca Test</title>
<link rel="stylesheet" href="/style/layout.css"/>
<link rel="stylesheet" href="/style/theme.css"/>
<script src="/lib/jquery-2.1.1.min.js"></script>
<script src="/lib/jquery.flot.min.js"></script>
<script src="/lib/core.js"></script>
<script src="/lib/charts.js"></script>
<script src="/lib/log.js"></script>
</head>
<body class="container_12">
<h1>ECA Dashboard Template</h1>
<div class="grid_6 vert_4">
<p>This is the dashboard template file. The easiest way to get started is to think up a simple name (let's say we take 'dashboard'). Now copy <code>template.py</code> to <code>{name}.py</code> start a new module (so that's <code>dashboard.py</code>) and copy <code>template_static</code> to <code>{name}_static</code>.
<p>Now you can run the new project with: <pre>python neca.py -s {name}.py</pre>
<p>Further documentation on the ECA system can be found at <a href="https://github.com/utwente-db/eca/wiki">github.com/utwente-db/eca/wiki</a>, and demos can be found in the <code>demos/</code> directory.
</div>
<div class="grid_6 vert_4">
<p>In the sample <code>template.py</code> (which comes with the dashboard you're looking at right now), you will find the rules that power this example.
<p>Rules are written in <a href="https://www.python.org/">Python</a> and work as follows:
<pre>@event("foo")
def action(context, event):
print("Event " + event.name + "!")
</pre>
The <code>@event</code> part tells the system to fire the action whenever the event 'foo' occurs. The <code>def action(context, event):</code> part defines a new action that takes two arguments: the context and the event. The rest of the code is the action body.
</div>
<div class="clear"></div>
<div class="grid_4">
<p>The graph to the right is continuously filled with data generated by the rules.
<p>In <code>template.py</code> you can see that an event called 'sample' is fired again and again to create new data points for the graph.
<p>These points are then sent to the browser with:
<pre>emit('sample',{
'action': 'add',
'value': sample
})</pre>
</div>
<div id="graph" class="grid_8 vert_4"></div>
<script>
// create a rolling chart block
block('#graph').rolling_chart({
memory: 150,
chart: {
yaxis: {
min: -100,
max: 100
},
xaxis: {
show: false
}
}
});
// connect sample event to graph
events.connect('sample', '#graph');
</script>
</body>
</html>

View File

@@ -0,0 +1,368 @@
(function($, block) {
// a simple rolling chart with memory
block.fn.rolling_chart = function(config) {
// combine default configuration with user configuration
var options = $.extend({
memory: 100,
// required!!
series: {
'default': {data: []}
},
// flot initialization options
options: {
xaxis: {
show: false
}
}
}, config);
// maintain state for this block
var data = {};
for(var k in options.series) {
data[k] = (options.series[k].data || []).slice();
}
// function to project our state to something the library understands
var prepare_data = function() {
var result = [];
// process each series
for(var k in data) {
var series = data[k];
var points = [];
// create point pairs and gap values
for(var i in series) {
if(series[i] == null) {
points.push(null);
} else {
points.push([i, series[i]]);
}
}
// combine state data with series configuration by user
result.push($.extend(options.series[k], {data: points}));
}
return result;
};
// initial setup of library state (also builds necessary HTML)
var plot = $.plot(this.$element, prepare_data(), options.options);
// register actions for this block
this.actions({
'add': function(e, message) {
// if the 'value' field is used, update all series (useful with a single series)
if(typeof message.values == 'undefined' && typeof message.value != 'undefined') {
message.values = {}
for(var k in options.series) {
message.values[k] = message.value;
}
}
// update all series
for(var k in options.series) {
// roll memory
if(data[k].length > options.memory) {
data[k] = data[k].slice(1);
}
// insert value or gap (in case of null)
data[k].push(message.values[k]);
}
// update HTML
plot.setData(prepare_data());
plot.setupGrid();
plot.draw();
}
});
// return element to allow further work
return this.$element;
}
//
//
//
// a simple linechart example
block.fn.linechart = function(config) {
var options = $.extend({
// required
series : {default:{}},
// flot initialization options
options : {}
}, config);
// create empty linechart with parameter options
var plot = $.plot(this.$element, [],options.options);
// dict containing the labels and values
var linedata_series = {};
var linedata_first;
var initline = function(series) {
linedata_first = undefined;
for(var k in series) {
var si = series[k];
si.data = [];
linedata_series[k] = si;
if ( linedata_first == undefined )
linedata_first = si;
}
}
initline(options.series);
var addline = function(label, values) {
var data;
if (linedata_series.hasOwnProperty(label))
data = linedata_series[label].data;
else
data = linedata_first.data;
for(var v in values) {
data.push(values[v]);
}
redraw();
}
var setline = function(label, values) {
if (linedata_series.hasOwnProperty(label))
linedata_series[label].data = values;
else
linedata_first.data = values;
redraw();
}
var redraw = function() {
var result = [];
for(var k in linedata_series) {
if (linedata_series.hasOwnProperty(k)) {
var line_serie = linedata_series[k];
result.push({label:k,data:line_serie.data});
}
}
plot.setData(result);
plot.setupGrid();
plot.draw();
}
var reset = function() {
initline(options.series);
}
this.actions({
'set': function(e, message) {
setline(message.series, message.value);
},
'add': function(e, message) {
addline(message.series, message.value);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
//
//
//
// a simple barchart example
block.fn.barchart = function(config) {
var options = $.extend({
filter_function : function(category,val,max) { return true; },
// required
series : { "default":{
data: {},
label: "default",
bars: {
show: true,
barWidth: 0.2,
align: "left"
}
} },
// flot initialization options
options: { xaxis: {
mode: "categories",
tickLength: 0
}}
}, config);
var bardata_series = options.series;
var bardata_first;
for (bardata_first in bardata_series) break;
var translate_bar = function() {
var result = [];
for(var k in bardata_series) {
if (bardata_series.hasOwnProperty(k)) {
var newserie = jQuery.extend({}, bardata_series[k]);
var newdata = [];
var data = newserie.data;
var max = 0;
for(var l in data) {
if (data.hasOwnProperty(l)) {
max = Math.max(max, data[l]);
}
}
for(var l in data) {
if (data.hasOwnProperty(l)) {
if ( options.filter_function(l,data[l],max) )
newdata.push([l,data[l]]);
}
}
newserie.data = newdata;
result.push(newserie);
}
}
return result;
}
var plot = $.plot(this.$element, translate_bar(), options.options);
var addbar = function(serie_label, category, value) {
var data;
if ( serie_label == undefined )
data = bardata_series[bardata_first].data;
else
data = bardata_series[serie_label].data;
if (data.hasOwnProperty(category))
data[category] = (data[category] + value);
else
data[category] = value;
redraw();
}
var setbar = function(serie_label, category, value) {
var data;
if ( serie_label == undefined )
data = bardata_series[bardata_first].data;
else
data = bardata_series[serie_label].data;
data[category] = value;
redraw();
}
var redraw = function() {
plot.setData(translate_bar());
plot.setupGrid();
plot.draw();
}
var reset = function() {
for(var k in bardata_series) {
if (bardata_series.hasOwnProperty(k)) {
bardata_series[k].data = {};
}
}
}
this.actions({
'set': function(e, message) {
setbar(message.series,message.value[0],message.value[1]);
},
'add': function(e, message) {
addbar(message.series,message.value[0],message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
//
//
//
// a simple piechart example
block.fn.piechart = function(config) {
var options = $.extend({
// see: http://www.flotcharts.org/flot/examples/series-pie/
filter_function : function(category,val,max) { return true; },
options : {
series: {
pie: {
show: true
}
},
// demo crashes with this option
// legend: { show: false }
}}, config);
// create empty piechart with parameter options
var plot = $.plot(this.$element, [],options.options);
// dict containing the labels and values
var piedata_dict = {};
var addpie = function(label, value) {
if (piedata_dict.hasOwnProperty(label))
piedata_dict[label] = (piedata_dict[label] + value);
else
piedata_dict[label] = value;
redraw();
}
var setpie = function(label, value) {
piedata_dict[label] = value;
redraw();
}
var redraw = function() {
var result = [];
var max = 0;
for(var k in piedata_dict) {
if (piedata_dict.hasOwnProperty(k)) {
max = Math.max(max, piedata_dict[k]);
}
}
for(var k in piedata_dict) {
if (piedata_dict.hasOwnProperty(k)) {
if ( options.filter_function(k,piedata_dict[k],max) )
result.push({label:k,data:piedata_dict[k]});
}
}
plot.setData(result);
plot.setupGrid();
plot.draw();
}
var reset = function() {
piedata_dict = {};
}
this.actions({
'set': function(e, message) {
setpie(message.value[0],message.value[1]);
},
'add': function(e, message) {
addpie(message.value[0],message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// return element to allow further work
return this.$element;
}
})(jQuery, block);

105
template_static/lib/core.js Normal file
View File

@@ -0,0 +1,105 @@
/*
Event stream handling.
See https://developer.mozilla.org/en-US/docs/Web/API/EventSource for a more
comprehensive explanation.
*/
events = {};
(function($, exports) {
var e = new EventSource('/events');
exports.connect = function(name, elements) {
// wrap to allow selector, jQuery object and DOM nodes
var $elements = $(elements);
// add listener that triggers events in DOM
this.listen(name, function(message) {
$elements.trigger('server-event', [message]);
});
};
exports.listen = function(name, callback) {
// add event listener to event stream
e.addEventListener(name, function(m) {
try {
var message = JSON.parse(m.data);
} catch(err) {
console.exception("Received malformed message: ",err);
return;
}
callback(message);
});
};
})(jQuery, events);
/*
** Block fucntion allows for quick creation of new block types.
*/
(function($) {
var ConstructionState = function($element) {
this.$element = $element;
// transfer all block constructors to scope
for(var b in block.fn) {
// prevent overrides
if(!(b in this)) {
// reference block type in this object
this[b] = block.fn[b];
}
}
};
ConstructionState.prototype.actions = function(actions_or_def, def) {
// handle function overloading
if(typeof actions_or_def == 'function') {
def = actions_or_def;
actions = {};
} else {
actions = actions_or_def;
}
// default actionless handler
if(typeof def == 'undefined') {
def = function(e, message) {
console.error("Received actionless server event." +
" Did you forget to set an action field?");
}
}
// dispatch all incoming server events
this.$element.on('server-event', function(e, message) {
if(!('action' in message)) {
$(this).trigger('_default.server-event', [message]);
} else {
$(this).trigger(message.action+'.server-event', [message]);
}
});
// bind all actions
this.$element.on('_default.server-event', def);
for(var k in actions) {
this.$element.on(k+'.server-event', actions[k]);
}
}
block = function(elements) {
// allow passing of selectors, jquery objects and DOM nodes
var $element = $(elements);
// actual work
if($element.length != 1) {
console.error("Must have one element to create block for." +
" Was given: '",elements,"'");
return;
}
return new ConstructionState($element);
}
block.fn = {};
})(jQuery);

View File

@@ -0,0 +1,72 @@
(function($, block) {
block.fn.form = function(config) {
var options = $.extend({
target: null,
callback: function() {}
}, config);
// see if we can grab the action from the form tag
if(options.target === null) {
var action = this.$element.find("form").attr('action');
if(typeof action !== 'undefined') {
options.target = action;
}
}
// check for sane config
if(options.target === null) {
console.log("The 'form' block requires a target option to know where to send the request.");
return this.$element;
}
// set up submit handler
var $block = this.$element;
$block.find("form").submit(function(event) {
var payload = {};
// handle simple fields
$block.find("textarea[name], select[name]").each(function() {
payload[this.name] = this.value;
});
// handle the more complex fields
$block.find("input[name]").each(function() {
switch($(this).attr('type')) {
// radio buttons usually have a single selected option per name
case 'radio':
if($(this).prop('checked')) {
payload[this.name] = this.value;
}
break;
// checkboxes are akin to a bitfield
case 'checkbox':
// build a map of checked values for this name
if(typeof payload[this.name] === 'undefined') {
payload[this.name] = [];
}
if($(this).prop('checked')) {
payload[this.name].push(this.value);
}
break;
// default to storing the value
default:
payload[this.name] = this.value;
break;
}
});
event.preventDefault();
// fire and forget the datablob
$.ajax(options.target,{
method: 'POST',
data: JSON.stringify(payload)
}).then(options.callback);
});
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,232 @@
/*!
* jQCloud Plugin for jQuery
*
* Version 1.0.4
*
* Copyright 2011, Luca Ongaro
* Licensed under the MIT license.
*
* Date: 2013-05-09 18:54:22 +0200
*/
(function( $ ) {
"use strict";
$.fn.jQCloud = function(word_array, options) {
// Reference to the container element
var $this = this;
// Namespace word ids to avoid collisions between multiple clouds
var cloud_namespace = $this.attr('id') || Math.floor((Math.random()*1000000)).toString(36);
// Default options value
var default_options = {
width: $this.width(),
height: $this.height(),
center: {
x: ((options && options.width) ? options.width : $this.width()) / 2.0,
y: ((options && options.height) ? options.height : $this.height()) / 2.0
},
delayedMode: word_array.length > 50,
shape: false, // It defaults to elliptic shape
encodeURI: true,
removeOverflowing: true
};
options = $.extend(default_options, options || {});
// Add the "jqcloud" class to the container for easy CSS styling, set container width/height
$this.addClass("jqcloud").width(options.width).height(options.height);
// Container's CSS position cannot be 'static'
if ($this.css("position") === "static") {
$this.css("position", "relative");
}
var drawWordCloud = function() {
// Helper function to test if an element overlaps others
var hitTest = function(elem, other_elems) {
// Pairwise overlap detection
var overlapping = function(a, b) {
if (Math.abs(2.0*a.offsetLeft + a.offsetWidth - 2.0*b.offsetLeft - b.offsetWidth) < a.offsetWidth + b.offsetWidth) {
if (Math.abs(2.0*a.offsetTop + a.offsetHeight - 2.0*b.offsetTop - b.offsetHeight) < a.offsetHeight + b.offsetHeight) {
return true;
}
}
return false;
};
var i = 0;
// Check elements for overlap one by one, stop and return false as soon as an overlap is found
for(i = 0; i < other_elems.length; i++) {
if (overlapping(elem, other_elems[i])) {
return true;
}
}
return false;
};
// Make sure every weight is a number before sorting
for (var i = 0; i < word_array.length; i++) {
word_array[i].weight = parseFloat(word_array[i].weight, 10);
}
// Sort word_array from the word with the highest weight to the one with the lowest
word_array.sort(function(a, b) { if (a.weight < b.weight) {return 1;} else if (a.weight > b.weight) {return -1;} else {return 0;} });
var step = (options.shape === "rectangular") ? 18.0 : 2.0,
already_placed_words = [],
aspect_ratio = options.width / options.height;
// Function to draw a word, by moving it in spiral until it finds a suitable empty place. This will be iterated on each word.
var drawOneWord = function(index, word) {
// Define the ID attribute of the span that will wrap the word, and the associated jQuery selector string
var word_id = cloud_namespace + "_word_" + index,
word_selector = "#" + word_id,
angle = 6.28 * Math.random(),
radius = 0.0,
// Only used if option.shape == 'rectangular'
steps_in_direction = 0.0,
quarter_turns = 0.0,
weight = 5,
custom_class = "",
inner_html = "",
word_span;
// Extend word html options with defaults
word.html = $.extend(word.html, {id: word_id});
// If custom class was specified, put them into a variable and remove it from html attrs, to avoid overwriting classes set by jQCloud
if (word.html && word.html["class"]) {
custom_class = word.html["class"];
delete word.html["class"];
}
// Check if min(weight) > max(weight) otherwise use default
if (word_array[0].weight > word_array[word_array.length - 1].weight) {
// Linearly map the original weight to a discrete scale from 1 to 10
weight = Math.round((word.weight - word_array[word_array.length - 1].weight) /
(word_array[0].weight - word_array[word_array.length - 1].weight) * 9.0) + 1;
}
word_span = $('<span>').attr(word.html).addClass('w' + weight + " " + custom_class);
// Append link if word.url attribute was set
if (word.link) {
// If link is a string, then use it as the link href
if (typeof word.link === "string") {
word.link = {href: word.link};
}
// Extend link html options with defaults
if ( options.encodeURI ) {
word.link = $.extend(word.link, { href: encodeURI(word.link.href).replace(/'/g, "%27") });
}
inner_html = $('<a>').attr(word.link).text(word.text);
} else {
inner_html = word.text;
}
word_span.append(inner_html);
// Bind handlers to words
if (!!word.handlers) {
for (var prop in word.handlers) {
if (word.handlers.hasOwnProperty(prop) && typeof word.handlers[prop] === 'function') {
$(word_span).bind(prop, word.handlers[prop]);
}
}
}
$this.append(word_span);
var width = word_span.width(),
height = word_span.height(),
left = options.center.x - width / 2.0,
top = options.center.y - height / 2.0;
// Save a reference to the style property, for better performance
var word_style = word_span[0].style;
word_style.position = "absolute";
word_style.left = left + "px";
word_style.top = top + "px";
while(hitTest(word_span[0], already_placed_words)) {
// option shape is 'rectangular' so move the word in a rectangular spiral
if (options.shape === "rectangular") {
steps_in_direction++;
if (steps_in_direction * step > (1 + Math.floor(quarter_turns / 2.0)) * step * ((quarter_turns % 4 % 2) === 0 ? 1 : aspect_ratio)) {
steps_in_direction = 0.0;
quarter_turns++;
}
switch(quarter_turns % 4) {
case 1:
left += step * aspect_ratio + Math.random() * 2.0;
break;
case 2:
top -= step + Math.random() * 2.0;
break;
case 3:
left -= step * aspect_ratio + Math.random() * 2.0;
break;
case 0:
top += step + Math.random() * 2.0;
break;
}
} else { // Default settings: elliptic spiral shape
radius += step;
angle += (index % 2 === 0 ? 1 : -1)*step;
left = options.center.x - (width / 2.0) + (radius*Math.cos(angle)) * aspect_ratio;
top = options.center.y + radius*Math.sin(angle) - (height / 2.0);
}
word_style.left = left + "px";
word_style.top = top + "px";
}
// Don't render word if part of it would be outside the container
if (options.removeOverflowing && (left < 0 || top < 0 || (left + width) > options.width || (top + height) > options.height)) {
word_span.remove()
return;
}
already_placed_words.push(word_span[0]);
// Invoke callback if existing
if ($.isFunction(word.afterWordRender)) {
word.afterWordRender.call(word_span);
}
};
var drawOneWordDelayed = function(index) {
index = index || 0;
if (!$this.is(':visible')) { // if not visible then do not attempt to draw
setTimeout(function(){drawOneWordDelayed(index);},10);
return;
}
if (index < word_array.length) {
drawOneWord(index, word_array[index]);
setTimeout(function(){drawOneWordDelayed(index + 1);}, 10);
} else {
if ($.isFunction(options.afterCloudRender)) {
options.afterCloudRender.call($this);
}
}
};
// Iterate drawOneWord on every word. The way the iteration is done depends on the drawing mode (delayedMode is true or false)
if (options.delayedMode){
drawOneWordDelayed();
}
else {
$.each(word_array, drawOneWord);
if ($.isFunction(options.afterCloudRender)) {
options.afterCloudRender.call($this);
}
}
};
// Delay execution so that the browser can render the page before the computatively intensive word cloud drawing
setTimeout(function(){drawWordCloud();}, 10);
return $this;
};
})(jQuery);

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
/* Flot plugin for plotting textual data or categories.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin
allows you to plot such a dataset directly.
To enable it, you must specify mode: "categories" on the axis with the textual
labels, e.g.
$.plot("#placeholder", data, { xaxis: { mode: "categories" } });
By default, the labels are ordered as they are met in the data series. If you
need a different ordering, you can specify "categories" on the axis options
and list the categories there:
xaxis: {
mode: "categories",
categories: ["February", "March", "April"]
}
If you need to customize the distances between the categories, you can specify
"categories" as an object mapping labels to values
xaxis: {
mode: "categories",
categories: { "February": 1, "March": 3, "April": 4 }
}
If you don't specify all categories, the remaining categories will be numbered
from the max value plus 1 (with a spacing of 1 between each).
Internally, the plugin works by transforming the input data through an auto-
generated mapping where the first category becomes 0, the second 1, etc.
Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this
is visible in hover and click events that return numbers rather than the
category labels). The plugin also overrides the tick generator to spit out the
categories as ticks instead of the values.
If you need to map a value back to its label, the mapping is always accessible
as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories.
*/(function(e){function n(e,t,n,r){var i=t.xaxis.options.mode=="categories",s=t.yaxis.options.mode=="categories";if(!i&&!s)return;var o=r.format;if(!o){var u=t;o=[],o.push({x:!0,number:!0,required:!0}),o.push({y:!0,number:!0,required:!0});if(u.bars.show||u.lines.show&&u.lines.fill){var a=!!(u.bars.show&&u.bars.zero||u.lines.show&&u.lines.zero);o.push({y:!0,number:!0,required:!1,defaultValue:0,autoscale:a}),u.bars.horizontal&&(delete o[o.length-1].y,o[o.length-1].x=!0)}r.format=o}for(var f=0;f<o.length;++f)o[f].x&&i&&(o[f].number=!1),o[f].y&&s&&(o[f].number=!1)}function r(e){var t=-1;for(var n in e)e[n]>t&&(t=e[n]);return t+1}function i(e){var t=[];for(var n in e.categories){var r=e.categories[n];r>=e.min&&r<=e.max&&t.push([r,n])}return t.sort(function(e,t){return e[0]-t[0]}),t}function s(t,n,r){if(t[n].options.mode!="categories")return;if(!t[n].categories){var s={},u=t[n].options.categories||{};if(e.isArray(u))for(var a=0;a<u.length;++a)s[u[a]]=a;else for(var f in u)s[f]=u[f];t[n].categories=s}t[n].options.ticks||(t[n].options.ticks=i),o(r,n,t[n].categories)}function o(e,t,n){var i=e.points,s=e.pointsize,o=e.format,u=t.charAt(0),a=r(n);for(var f=0;f<i.length;f+=s){if(i[f]==null)continue;for(var l=0;l<s;++l){var c=i[f+l];if(c==null||!o[l][u])continue;c in n||(n[c]=a,++a),i[f+l]=n[c]}}}function u(e,t,n){s(t,"xaxis",n),s(t,"yaxis",n)}function a(e){e.hooks.processRawData.push(n),e.hooks.processDatapoints.push(u)}var t={xaxis:{categories:null},yaxis:{categories:null}};e.plot.plugins.push({init:a,options:t,name:"categories",version:"1.0"})})(jQuery);

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,820 @@
/* Flot plugin for rendering pie charts.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin assumes that each series has a single data value, and that each
value is a positive integer or zero. Negative numbers don't make sense for a
pie chart, and have unpredictable results. The values do NOT need to be
passed in as percentages; the plugin will calculate the total and per-slice
percentages internally.
* Created by Brian Medendorp
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
The plugin supports these options:
series: {
pie: {
show: true/false
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
offset: {
top: integer value to move the pie up or down
left: integer value to move the pie left or right, or 'auto'
},
stroke: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
width: integer pixel width of the stroke
},
label: {
show: true/false, or 'auto'
formatter: a user-defined function that modifies the text/style of the label text
radius: 0-1 for percentage of fullsize, or a specified pixel length
background: {
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
opacity: 0-1
},
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
},
combine: {
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
label: any text value of what the combined slice should be labeled
}
highlight: {
opacity: 0-1
}
}
}
More detail and specific examples can be found in the included HTML file.
*/
(function($) {
// Maximum redraw attempts when fitting labels within the plot
var REDRAW_ATTEMPTS = 10;
// Factor by which to shrink the pie when fitting labels within the plot
var REDRAW_SHRINK = 0.95;
function init(plot) {
var canvas = null,
target = null,
options = null,
maxRadius = null,
centerLeft = null,
centerTop = null,
processed = false,
ctx = null;
// interactive variables
var highlights = [];
// add hook to determine if pie plugin in enabled, and then perform necessary operations
plot.hooks.processOptions.push(function(plot, options) {
if (options.series.pie.show) {
options.grid.show = false;
// set labels.show
if (options.series.pie.label.show == "auto") {
if (options.legend.show) {
options.series.pie.label.show = false;
} else {
options.series.pie.label.show = true;
}
}
// set radius
if (options.series.pie.radius == "auto") {
if (options.series.pie.label.show) {
options.series.pie.radius = 3/4;
} else {
options.series.pie.radius = 1;
}
}
// ensure sane tilt
if (options.series.pie.tilt > 1) {
options.series.pie.tilt = 1;
} else if (options.series.pie.tilt < 0) {
options.series.pie.tilt = 0;
}
}
});
plot.hooks.bindEvents.push(function(plot, eventHolder) {
var options = plot.getOptions();
if (options.series.pie.show) {
if (options.grid.hoverable) {
eventHolder.unbind("mousemove").mousemove(onMouseMove);
}
if (options.grid.clickable) {
eventHolder.unbind("click").click(onClick);
}
}
});
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
var options = plot.getOptions();
if (options.series.pie.show) {
processDatapoints(plot, series, data, datapoints);
}
});
plot.hooks.drawOverlay.push(function(plot, octx) {
var options = plot.getOptions();
if (options.series.pie.show) {
drawOverlay(plot, octx);
}
});
plot.hooks.draw.push(function(plot, newCtx) {
var options = plot.getOptions();
if (options.series.pie.show) {
draw(plot, newCtx);
}
});
function processDatapoints(plot, series, datapoints) {
if (!processed) {
processed = true;
canvas = plot.getCanvas();
target = $(canvas).parent();
options = plot.getOptions();
plot.setData(combine(plot.getData()));
}
}
function combine(data) {
var total = 0,
combined = 0,
numCombined = 0,
color = options.series.pie.combine.color,
newdata = [];
// Fix up the raw data from Flot, ensuring the data is numeric
for (var i = 0; i < data.length; ++i) {
var value = data[i].data;
// If the data is an array, we'll assume that it's a standard
// Flot x-y pair, and are concerned only with the second value.
// Note how we use the original array, rather than creating a
// new one; this is more efficient and preserves any extra data
// that the user may have stored in higher indexes.
if ($.isArray(value) && value.length == 1) {
value = value[0];
}
if ($.isArray(value)) {
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
value[1] = +value[1];
} else {
value[1] = 0;
}
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
value = [1, +value];
} else {
value = [1, 0];
}
data[i].data = [value];
}
// Sum up all the slices, so we can calculate percentages for each
for (var i = 0; i < data.length; ++i) {
total += data[i].data[0][1];
}
// Count the number of slices with percentages below the combine
// threshold; if it turns out to be just one, we won't combine.
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (value / total <= options.series.pie.combine.threshold) {
combined += value;
numCombined++;
if (!color) {
color = data[i].color;
}
}
}
for (var i = 0; i < data.length; ++i) {
var value = data[i].data[0][1];
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
newdata.push(
$.extend(data[i], { /* extend to allow keeping all other original data values
and using them e.g. in labelFormatter. */
data: [[1, value]],
color: data[i].color,
label: data[i].label,
angle: value * Math.PI * 2 / total,
percent: value / (total / 100)
})
);
}
}
if (numCombined > 1) {
newdata.push({
data: [[1, combined]],
color: color,
label: options.series.pie.combine.label,
angle: combined * Math.PI * 2 / total,
percent: combined / (total / 100)
});
}
return newdata;
}
function draw(plot, newCtx) {
if (!target) {
return; // if no series were passed
}
var canvasWidth = plot.getPlaceholder().width(),
canvasHeight = plot.getPlaceholder().height(),
legendWidth = target.children().filter(".legend").children().width() || 0;
ctx = newCtx;
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
// When combining smaller slices into an 'other' slice, we need to
// add a new series. Since Flot gives plugins no way to modify the
// list of series, the pie plugin uses a hack where the first call
// to processDatapoints results in a call to setData with the new
// list of series, then subsequent processDatapoints do nothing.
// The plugin-global 'processed' flag is used to control this hack;
// it starts out false, and is set to true after the first call to
// processDatapoints.
// Unfortunately this turns future setData calls into no-ops; they
// call processDatapoints, the flag is true, and nothing happens.
// To fix this we'll set the flag back to false here in draw, when
// all series have been processed, so the next sequence of calls to
// processDatapoints once again starts out with a slice-combine.
// This is really a hack; in 0.9 we need to give plugins a proper
// way to modify series before any processing begins.
processed = false;
// calculate maximum radius and center point
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
centerLeft = canvasWidth / 2;
if (options.series.pie.offset.left == "auto") {
if (options.legend.position.match("w")) {
centerLeft += legendWidth / 2;
} else {
centerLeft -= legendWidth / 2;
}
if (centerLeft < maxRadius) {
centerLeft = maxRadius;
} else if (centerLeft > canvasWidth - maxRadius) {
centerLeft = canvasWidth - maxRadius;
}
} else {
centerLeft += options.series.pie.offset.left;
}
var slices = plot.getData(),
attempts = 0;
// Keep shrinking the pie's radius until drawPie returns true,
// indicating that all the labels fit, or we try too many times.
do {
if (attempts > 0) {
maxRadius *= REDRAW_SHRINK;
}
attempts += 1;
clear();
if (options.series.pie.tilt <= 0.8) {
drawShadow();
}
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
if (attempts >= REDRAW_ATTEMPTS) {
clear();
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
}
if (plot.setSeries && plot.insertLegend) {
plot.setSeries(slices);
plot.insertLegend();
}
// we're actually done at this point, just defining internal functions at this point
function clear() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
target.children().filter(".pieLabel, .pieLabelBackground").remove();
}
function drawShadow() {
var shadowLeft = options.series.pie.shadow.left;
var shadowTop = options.series.pie.shadow.top;
var edge = 10;
var alpha = options.series.pie.shadow.alpha;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
return; // shadow would be outside canvas, so don't draw it
}
ctx.save();
ctx.translate(shadowLeft,shadowTop);
ctx.globalAlpha = alpha;
ctx.fillStyle = "#000";
// center and rotate to starting position
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//radius -= edge;
for (var i = 1; i <= edge; i++) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
ctx.fill();
radius -= i;
}
ctx.restore();
}
function drawPie() {
var startAngle = Math.PI * options.series.pie.startAngle;
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
// center and rotate to starting position
ctx.save();
ctx.translate(centerLeft,centerTop);
ctx.scale(1, options.series.pie.tilt);
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
// draw slices
ctx.save();
var currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
slices[i].startAngle = currentAngle;
drawSlice(slices[i].angle, slices[i].color, true);
}
ctx.restore();
// draw slice outlines
if (options.series.pie.stroke.width > 0) {
ctx.save();
ctx.lineWidth = options.series.pie.stroke.width;
currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i) {
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
}
ctx.restore();
}
// draw donut hole
drawDonutHole(ctx);
ctx.restore();
// Draw the labels, returning true if they fit within the plot
if (options.series.pie.label.show) {
return drawLabels();
} else return true;
function drawSlice(angle, color, fill) {
if (angle <= 0 || isNaN(angle)) {
return;
}
if (fill) {
ctx.fillStyle = color;
} else {
ctx.strokeStyle = color;
ctx.lineJoin = "round";
}
ctx.beginPath();
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
ctx.moveTo(0, 0); // Center of the pie
}
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false);
ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false);
ctx.closePath();
//ctx.rotate(angle); // This doesn't work properly in Opera
currentAngle += angle;
if (fill) {
ctx.fill();
} else {
ctx.stroke();
}
}
function drawLabels() {
var currentAngle = startAngle;
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
for (var i = 0; i < slices.length; ++i) {
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
if (!drawLabel(slices[i], currentAngle, i)) {
return false;
}
}
currentAngle += slices[i].angle;
}
return true;
function drawLabel(slice, startAngle, index) {
if (slice.data[0][1] == 0) {
return true;
}
// format label text
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
if (lf) {
text = lf(slice.label, slice);
} else {
text = slice.label;
}
if (plf) {
text = plf(text, slice);
}
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
target.append(html);
var label = target.children("#pieLabel" + index);
var labelTop = (y - label.height() / 2);
var labelLeft = (x - label.width() / 2);
label.css("top", labelTop);
label.css("left", labelLeft);
// check to make sure that the label is not outside the canvas
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
return false;
}
if (options.series.pie.label.background.opacity != 0) {
// put in the transparent background separately to avoid blended labels and label boxes
var c = options.series.pie.label.background.color;
if (c == null) {
c = slice.color;
}
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
.css("opacity", options.series.pie.label.background.opacity)
.insertBefore(label);
}
return true;
} // end individual label function
} // end drawLabels function
} // end drawPie function
} // end draw function
// Placed here because it needs to be accessed from multiple locations
function drawDonutHole(layer) {
if (options.series.pie.innerRadius > 0) {
// subtract the center
layer.save();
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
layer.beginPath();
layer.fillStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.fill();
layer.closePath();
layer.restore();
// add inner stroke
layer.save();
layer.beginPath();
layer.strokeStyle = options.series.pie.stroke.color;
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
layer.stroke();
layer.closePath();
layer.restore();
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
}
}
//-- Additional Interactive related functions --
function isPointInPoly(poly, pt) {
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
&& (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
&& (c = !c);
return c;
}
function findNearbySlice(mouseX, mouseY) {
var slices = plot.getData(),
options = plot.getOptions(),
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
x, y;
for (var i = 0; i < slices.length; ++i) {
var s = slices[i];
if (s.pie.show) {
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0); // Center of the pie
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
ctx.closePath();
x = mouseX - centerLeft;
y = mouseY - centerTop;
if (ctx.isPointInPath) {
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
} else {
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
var p1X = radius * Math.cos(s.startAngle),
p1Y = radius * Math.sin(s.startAngle),
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
p5X = radius * Math.cos(s.startAngle + s.angle),
p5Y = radius * Math.sin(s.startAngle + s.angle),
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
arrPoint = [x, y];
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
if (isPointInPoly(arrPoly, arrPoint)) {
ctx.restore();
return {
datapoint: [s.percent, s.data],
dataIndex: 0,
series: s,
seriesIndex: i
};
}
}
ctx.restore();
}
}
return null;
}
function onMouseMove(e) {
triggerClickHoverEvent("plothover", e);
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e);
}
// trigger click or hover event (they send the same parameters so we share their code)
function triggerClickHoverEvent(eventname, e) {
var offset = plot.offset();
var canvasX = parseInt(e.pageX - offset.left);
var canvasY = parseInt(e.pageY - offset.top);
var item = findNearbySlice(canvasX, canvasY);
if (options.grid.autoHighlight) {
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.auto == eventname && !(item && h.series == item.series)) {
unhighlight(h.series);
}
}
}
// highlight the slice
if (item) {
highlight(item.series, eventname);
}
// trigger any hover bind events
var pos = { pageX: e.pageX, pageY: e.pageY };
target.trigger(eventname, [pos, item]);
}
function highlight(s, auto) {
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i == -1) {
highlights.push({ series: s, auto: auto });
plot.triggerRedrawOverlay();
} else if (!auto) {
highlights[i].auto = false;
}
}
function unhighlight(s) {
if (s == null) {
highlights = [];
plot.triggerRedrawOverlay();
}
//if (typeof s == "number") {
// s = series[s];
//}
var i = indexOfHighlight(s);
if (i != -1) {
highlights.splice(i, 1);
plot.triggerRedrawOverlay();
}
}
function indexOfHighlight(s) {
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.series == s)
return i;
}
return -1;
}
function drawOverlay(plot, octx) {
var options = plot.getOptions();
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
octx.save();
octx.translate(centerLeft, centerTop);
octx.scale(1, options.series.pie.tilt);
for (var i = 0; i < highlights.length; ++i) {
drawHighlight(highlights[i].series);
}
drawDonutHole(octx);
octx.restore();
function drawHighlight(series) {
if (series.angle <= 0 || isNaN(series.angle)) {
return;
}
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
octx.beginPath();
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
octx.moveTo(0, 0); // Center of the pie
}
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
octx.closePath();
octx.fill();
}
}
} // end init (plugin body)
// define pie specific options and their default values
var options = {
series: {
pie: {
show: false,
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
innerRadius: 0, /* for donut */
startAngle: 3/2,
tilt: 1,
shadow: {
left: 5, // shadow left offset
top: 15, // shadow top offset
alpha: 0.02 // shadow alpha
},
offset: {
top: 0,
left: "auto"
},
stroke: {
color: "#fff",
width: 1
},
label: {
show: "auto",
formatter: function(label, slice) {
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
}, // formatter function
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
background: {
color: null,
opacity: 0
},
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
},
combine: {
threshold: -1, // percentage at which to combine little slices into one larger slice
color: null, // color to give the new slice (auto-generated if null)
label: "Other" // label to give the new slice
},
highlight: {
//color: "#fff", // will add this functionality once parseColor is available
opacity: 0.5
}
}
}
};
$.plot.plugins.push({
init: init,
options: options,
name: "pie",
version: "1.1"
});
})(jQuery);

View File

@@ -0,0 +1,14 @@
(function($, block) {
block.fn.log = function(config) {
this.$element.addClass('block log').append('<ul>');
this.actions(function(e, message){
$ul = $('ul:first-child', this);
$ul.append('<li>');
$ul.find("> li:last-child").text(message.text);
$(this).scrollTop(1000000);
});
return this.$element;
};
})(jQuery, block);

View File

@@ -0,0 +1,128 @@
(function($, block) {
// Entity formatters for use by tweet list
var entity_formatters = {
'urls': function(e) {
return '<a href="' + e.url + '">' + e.display_url + '</a>';
},
'user_mentions': function(e) {
return '<a href="https://twitter.com/'+e.screen_name+'">@'+e.screen_name+'</a>';
},
'hashtags': function(e) {
return '<a href="https://twitter.com/hashtag/'+e.text+'?src=hash">#' +e.text+'</a>';
},
'default': function(e) {
return '{ENTITY}';
}
};
// processes entities for the given message and entity object
var process_entities = function(message, entities) {
// short-circuit failure mode
if(typeof entities === 'undefined') {
return message;
}
// build list of entities sorted on starting index
var es = [];
$.each(entities, function(t, ts) {
$.each(ts, function(_, e) {
e['type'] = t;
es.push(e);
});
});
es.sort(function(a,b) {
return a['indices'][0] - b['indices'][0];
});
// process entities one-by-one in order of appearance
var marker = 0;
var result = "";
for(var i in es) {
var e = es[i];
var start = e['indices'][0];
var stop = e['indices'][1];
//copy string content
result += message.substring(marker, start);
//process entity (through formatter or no-op function)
var formatter = entity_formatters[e.type]
|| function(e) { return message.substring(start,stop) };
result += formatter(e);
// update marker location
marker = stop;
}
// append tail of message
result += message.substring(marker, message.length);
return result;
}
block.fn.tweets = function(config) {
var options = $.extend({
memory: 20
}, config);
// create the necessary HTML in the block container
this.$element.append('<ol class="tweet-list stream-items"></ol>');
// store list for later
var $list = this.$element.find('ol');
// register default handler for handling tweet data
this.actions(function(e, tweet){
var $item = $('<li class="stream-item"></li>');
var $tweet = $('<div class="tweet"></div>');
var $content = $('<div class="content"></div>');
var $header = $('<div class="stream-item-header"></div>');
// Build a tag image and header:
var $account = $('<a class="account-group"></a>');
$account.attr("href", "http://twitter.com/" + tweet.user.screen_name);
var $avatar = $("<img>").addClass("avatar");
$avatar.attr("src", tweet.user.profile_image_url);
$account.append($avatar);
$account.append($('<strong class="fullname">' + tweet.user.name + '</strong>'));
$account.append($('<span>&nbsp;</span>'));
$account.append($('<span class="username"><s>@</s><b>' + tweet.user.screen_name + '</b></span>'));
$header.append($account);
// Build timestamp:
var $time = $('<small class="time"></small>');
$time.append($('<span>' + tweet.created_at + '</span>'));
$header.append($time);
$content.append($header);
// Build contents:
var text = process_entities(tweet.text, tweet.entities);
var $text = $('<p class="tweet-text">' + text + '</p>');
$content.append($text);
// Build outer structure of containing divs:
$tweet.append($content);
$item.append($tweet);
// place new tweet in front of list
$list.prepend($item);
// remove stale tweets
if ($list.children().length > options.memory) {
$list.children().last().remove();
}
});
return this.$element;
};
})(jQuery, block);

View File

@@ -0,0 +1,88 @@
(function($, block) {
// a simple wordcloud example
block.fn.wordcloud = function(config) {
var options = $.extend({
filter_function : function(cat,val,max) { return true; },
weight_function : function(cat,val,max) { return val; },
options : {}
}, config);
var $container = $(this.$element);
// dict containing the labels and values
var worddata_dict = {};
var dirty = false;
var addword = function(label, value) {
if (worddata_dict.hasOwnProperty(label)) {
worddata_dict[label] += value;
} else {
worddata_dict[label] = value;
}
flag_dirty();
}
var setword = function(label, value) {
worddata_dict[label] = value;
flag_dirty();
}
var flag_dirty = function() {
dirty = true;
}
var redraw = function() {
if(!dirty) {
window.setTimeout(redraw, 500);
return;
}
var result = [];
var max = 0;
for (var k in worddata_dict) {
if (worddata_dict.hasOwnProperty(k)) {
max = Math.max(max, worddata_dict[k]);
}
}
for (var k in worddata_dict) {
if (worddata_dict.hasOwnProperty(k)) {
var val = worddata_dict[k];
if (options.filter_function(k,val,max)) {
result.push({
text: k,
weight: options.weight_function(k,val,max)
});
}
}
}
$($container).empty().jQCloud(result,$.extend(options.options,{delayedMode: false}));
dirty = false;
window.setTimeout(redraw, 500);
}
var reset = function() {
worddata_dict = {};
}
this.actions({
'set': function(e, message) {
setword(message.value[0], message.value[1]);
},
'add': function(e, message) {
addword(message.value[0], message.value[1]);
},
'reset': function(e, message) {
reset();
}
});
// start redraw loop
window.setTimeout(redraw, 500);
// return element to allow further work
return this.$element;
}
})(jQuery, block);

View File

@@ -0,0 +1,374 @@
/*
Variable Grid System.
Learn more ~ http://www.spry-soft.com/grids/
Based on 960 Grid System - http://960.gs/
Licensed under GPL and MIT.
*/
/*
Forces backgrounds to span full width,
even if there is horizontal scrolling.
Increase this if your layout is wider.
Note: IE6 works fine without this fix.
*/
body {
min-width: 960px;
}
/* Containers
----------------------------------------------------------------------------------------------------*/
.container_12 {
margin-left: auto;
margin-right: auto;
width: 960px;
}
/* Grid >> Global
----------------------------------------------------------------------------------------------------*/
.grid_1,
.grid_2,
.grid_3,
.grid_4,
.grid_5,
.grid_6,
.grid_7,
.grid_8,
.grid_9,
.grid_10,
.grid_11,
.grid_12 {
display:inline;
float: left;
position: relative;
margin-left: 10px;
margin-right: 10px;
}
.push_1, .pull_1,
.push_2, .pull_2,
.push_3, .pull_3,
.push_4, .pull_4,
.push_5, .pull_5,
.push_6, .pull_6,
.push_7, .pull_7,
.push_8, .pull_8,
.push_9, .pull_9,
.push_10, .pull_10,
.push_11, .pull_11,
.push_12, .pull_12 {
position:relative;
}
/* Grid >> Children (Alpha ~ First, Omega ~ Last)
----------------------------------------------------------------------------------------------------*/
.alpha {
margin-left: 0;
}
.omega {
margin-right: 0;
}
/* Grid >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .grid_1 {
width:60px;
}
.container_12 .grid_2 {
width:140px;
}
.container_12 .grid_3 {
width:220px;
}
.container_12 .grid_4 {
width:300px;
}
.container_12 .grid_5 {
width:380px;
}
.container_12 .grid_6 {
width:460px;
}
.container_12 .grid_7 {
width:540px;
}
.container_12 .grid_8 {
width:620px;
}
.container_12 .grid_9 {
width:700px;
}
.container_12 .grid_10 {
width:780px;
}
.container_12 .grid_11 {
width:860px;
}
.container_12 .grid_12 {
width:940px;
}
/* Prefix Extra Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .prefix_1 {
padding-left:80px;
}
.container_12 .prefix_2 {
padding-left:160px;
}
.container_12 .prefix_3 {
padding-left:240px;
}
.container_12 .prefix_4 {
padding-left:320px;
}
.container_12 .prefix_5 {
padding-left:400px;
}
.container_12 .prefix_6 {
padding-left:480px;
}
.container_12 .prefix_7 {
padding-left:560px;
}
.container_12 .prefix_8 {
padding-left:640px;
}
.container_12 .prefix_9 {
padding-left:720px;
}
.container_12 .prefix_10 {
padding-left:800px;
}
.container_12 .prefix_11 {
padding-left:880px;
}
/* Suffix Extra Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .suffix_1 {
padding-right:80px;
}
.container_12 .suffix_2 {
padding-right:160px;
}
.container_12 .suffix_3 {
padding-right:240px;
}
.container_12 .suffix_4 {
padding-right:320px;
}
.container_12 .suffix_5 {
padding-right:400px;
}
.container_12 .suffix_6 {
padding-right:480px;
}
.container_12 .suffix_7 {
padding-right:560px;
}
.container_12 .suffix_8 {
padding-right:640px;
}
.container_12 .suffix_9 {
padding-right:720px;
}
.container_12 .suffix_10 {
padding-right:800px;
}
.container_12 .suffix_11 {
padding-right:880px;
}
/* Push Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .push_1 {
left:80px;
}
.container_12 .push_2 {
left:160px;
}
.container_12 .push_3 {
left:240px;
}
.container_12 .push_4 {
left:320px;
}
.container_12 .push_5 {
left:400px;
}
.container_12 .push_6 {
left:480px;
}
.container_12 .push_7 {
left:560px;
}
.container_12 .push_8 {
left:640px;
}
.container_12 .push_9 {
left:720px;
}
.container_12 .push_10 {
left:800px;
}
.container_12 .push_11 {
left:880px;
}
/* Pull Space >> 12 Columns
----------------------------------------------------------------------------------------------------*/
.container_12 .pull_1 {
left:-80px;
}
.container_12 .pull_2 {
left:-160px;
}
.container_12 .pull_3 {
left:-240px;
}
.container_12 .pull_4 {
left:-320px;
}
.container_12 .pull_5 {
left:-400px;
}
.container_12 .pull_6 {
left:-480px;
}
.container_12 .pull_7 {
left:-560px;
}
.container_12 .pull_8 {
left:-640px;
}
.container_12 .pull_9 {
left:-720px;
}
.container_12 .pull_10 {
left:-800px;
}
.container_12 .pull_11 {
left:-880px;
}
/* `Clear Floated Elements
----------------------------------------------------------------------------------------------------*/
/* http://sonspring.com/journal/clearing-floats */
.clear {
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}
/* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */
.clearfix:before,
.clearfix:after {
content: '\0020';
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}
.clearfix:after {
clear: both;
}
/*
The following zoom:1 rule is specifically for IE6 + IE7.
Move to separate stylesheet if invalid CSS is a problem.
*/
.clearfix {
zoom: 1;
}

View File

@@ -0,0 +1,69 @@
/*
** Base layout:
** Grid layout + vertical sizing classes sized to match
*/
/* Grid layout based on (http://960.gs/) */
@import url(grid.css);
/* Vertical classes */
.grid_1, .vert_1,
.grid_2, .vert_2,
.grid_3, .vert_3,
.grid_4, .vert_4,
.grid_5, .vert_5,
.grid_6, .vert_6,
.grid_7, .vert_7,
.grid_8, .vert_8,
.grid_9, .vert_9,
.grid_10, .vert_10,
.grid_11, .vert_11,
.grid_12, .vert_12 {
margin-top: 10px;
margin-bottom: 10px;
}
.container_12 .vert_1 { height:60px; }
.container_12 .vert_2 { height:140px; }
.container_12 .vert_3 { height:220px; }
.container_12 .vert_4 { height:300px; }
.container_12 .vert_5 { height:380px; }
.container_12 .vert_6 { height:460px; }
.container_12 .vert_7 { height:540px; }
.container_12 .vert_8 { height:620px; }
.container_12 .vert_9 { height:700px; }
.container_12 .vert_10 { height:780px; }
.container_12 .vert_11 { height:860px; }
.container_12 .vert_12 { height:940px; }
/* Layout details */
p:first-child {
margin-top: 0;
}
/* Log block */
.block.log {
overflow: auto;
}
/* Tweets block */
.tweet-list * {
margin: 0;
padding: 0;
}
.tweet-list.stream-items {
margin: 0;
padding: 0;
overflow: auto;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,160 @@
/* Basic style & theme*/
body {
font-family: sans-serif;
}
p {
text-align: justify;
}
code {
background-color: #eee;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0 0.2em;
}
pre {
padding: 0.5em 1.5em;
background-color: #eee;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
/* Devevlopment helpers */
.debug_red { background-color: rgba(255,0,0,0.5); }
.debug_green { background-color: rgba(0,255,0,0.5); }
.debug_blue { background-color: rgba(0,0,255,0.5); }
/* Tweets block */
.tweet-list.stream-items {
position: relative;
background-color: #fff;
list-style: none;
color: #333;
font-size: 14px;
line-height: 18px;
font-family: arial, sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.tweet-list .stream-item {
background: #fff;
background-clip: padding-box;
}
.tweet-list .stream-item:hover {
background-color: #eee;
}
.tweet-list li.stream-item {
line-height: inherit
}
.tweet-list .tweet {
position: relative;
min-height: 51px;
padding: 9px 12px;
}
.tweet-list .stream-item + .stream-item {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.tweet-list .tweet p {
word-wrap: break-word
}
.tweet-list .tweet .details {
display: inline-block;
margin-right: 2px
}
.tweet-list .tweet .context a {
color: #999
}
.tweet-list .stream-item .content {
margin-left: 58px
}
.tweet-list .stream-item-header .avatar {
float: left;
margin-top: 3px;
margin-left: -58px
}
.tweet-list .account-group {
color: #999
}
.tweet-list a {
color: #0084b4;
text-decoration: none
}
.tweet-list a:focus {
outline: 0
}
.tweet-list a:hover,
.tweet-list a:focus {
color: #0084b4;
text-decoration: underline
}
.tweet-list a.account-group:hover,
.tweet-list a.account-group:focus {
text-decoration: none;
}
.tweet-list a.account-group:hover .fullname,
.tweet-list a.account-group:focus .fullname {
text-decoration: underline;
}
.tweet-list .avatar {
width: 48px;
height: 48px;
border-radius: 5px;
-moz-force-broken-image-icon: 1
}
.tweet-list .fullname {
font-weight: bold;
color: #333;
}
.tweet-list .username {
font-size: 12px;
color: #999
}
.tweet-list .username s {
color: #bbb
}
.tweet-list s {
text-decoration: none
}
.tweet-list b {
font-weight: normal
}
.tweet-list .tweet .time {
position: relative;
float: right;
margin-top: 1px;
color: #bbb
}
.tweet-list .tweet-timestamp {
color: #999
}
.tweet-list .tweet .tweet-text {
white-space: pre-wrap
}

View File

@@ -0,0 +1,49 @@
/* fonts */
div.jqcloud {
font-family: "Helvetica", "Arial", sans-serif;
font-size: 10px;
line-height: normal;
}
div.jqcloud a {
font-size: inherit;
text-decoration: none;
}
div.jqcloud span.w10 { font-size: 550%; }
div.jqcloud span.w9 { font-size: 500%; }
div.jqcloud span.w8 { font-size: 450%; }
div.jqcloud span.w7 { font-size: 400%; }
div.jqcloud span.w6 { font-size: 350%; }
div.jqcloud span.w5 { font-size: 300%; }
div.jqcloud span.w4 { font-size: 250%; }
div.jqcloud span.w3 { font-size: 200%; }
div.jqcloud span.w2 { font-size: 150%; }
div.jqcloud span.w1 { font-size: 100%; }
/* colors */
div.jqcloud { color: #09f; }
div.jqcloud a { color: inherit; }
div.jqcloud a:hover { color: #0df; }
div.jqcloud a:hover { color: #0cf; }
div.jqcloud span.w10 { color: #0cf; }
div.jqcloud span.w9 { color: #0cf; }
div.jqcloud span.w8 { color: #0cf; }
div.jqcloud span.w7 { color: #39d; }
div.jqcloud span.w6 { color: #90c5f0; }
div.jqcloud span.w5 { color: #90a0dd; }
div.jqcloud span.w4 { color: #90c5f0; }
div.jqcloud span.w3 { color: #a0ddff; }
div.jqcloud span.w2 { color: #99ccee; }
div.jqcloud span.w1 { color: #aab5f0; }
/* layout */
div.jqcloud {
overflow: hidden;
position: relative;
}
div.jqcloud span { padding: 0; }

57
tweet2arff.py Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import argparse
import json
import eca.arff
def file_type(mode):
"""Acts as an output file type for argparse. Always uses utf-8 encoding."""
def handler(name):
if name == '-':
import sys
if 'r' in mode:
return sys.stdin
elif 'w' in mode:
return sys.stdout
else:
raise argparse.ArgumentTypeError("can't use mode '{}' for stdin/stdout".format(mode))
try:
return open(name, mode, encoding='utf-8')
except OSError as e:
raise argparse.ArgumentTypeError("can't open '{}': {}".format(name, e))
return handler
def rows(tweets):
"""
This Generator function takes an opened data file with one JSON object
representing a tweet per line, and generates a row for each tweet.
"""
for line in tweets:
tweet = json.loads(line)
yield {'tweet': tweet['text']}
def main():
"""
Main program entry point.
"""
parser = argparse.ArgumentParser(description='Tweet data to ARFF converter')
parser.add_argument('file', type=file_type('r'), help='Twitter data source')
parser.add_argument('output', type=file_type('w'), help='Output file')
args = parser.parse_args()
# attribute description
fields = [
eca.arff.Field('tweet', eca.arff.Text()),
eca.arff.Field('@@class@@', eca.arff.Nominal(['a','b','c']))
]
# create item generator
items = rows(args.file)
eca.arff.save(args.output, fields, items, name='ARFF for '+args.file.name)
if __name__ == '__main__':
main()