This is an idea for a dynamic logo for the embryonic fedmsg-notifications system.
A development instance is available at http://209.132.184.212/
*.pyc |
This is an idea for a dynamic logo for the embryonic fedmsg-notifications system.
A development instance is available at http://209.132.184.212/
path.link { | |
fill: none; | |
stroke: #294172; | |
stroke-width: 1.5px; | |
} | |
marker#output { | |
fill: #3c6eb4; | |
} | |
path.link.output { | |
stroke: #3c6eb4; | |
stroke-dasharray: 5,5; | |
} | |
text { | |
font: 10px sans-serif; | |
pointer-events: none; | |
} | |
text.shadow { | |
stroke: #fff; | |
stroke-width: 3px; | |
stroke-opacity: .8; | |
} | |
.node image { | |
// Can we make the images circular? Not with css apparently. | |
} | |
// It would be cool to somehow differentiate these things | |
.group-1 { | |
} | |
.group-2 { | |
} | |
.group-3 { | |
} |
/* Generate this with a python script */ | |
// Compute the distinct nodes from the links. | |
//links.forEach(function(link) { | |
// link.source = nodes[link.source] || (nodes[link.source] = {name: link.source}); | |
// link.target = nodes[link.target] || (nodes[link.target] = {name: link.target}); | |
//}); | |
var w = 600, | |
h = 600; | |
d3.json("graph.json", function(json) { | |
var force = d3.layout.force() | |
.nodes(json.nodes) | |
.links(json.links) | |
.size([w, h]) | |
.linkDistance(60) | |
.charge(-600) | |
.on("tick", tick) | |
.start(); | |
var svg = d3.select("body").append("svg:svg") | |
.attr("width", w) | |
.attr("height", h); | |
// Per-type markers, as they don't inherit styles. | |
svg.append("svg:defs").selectAll("marker") | |
.data(["input", "output"]) | |
.enter().append("svg:marker") | |
.attr("id", String) | |
.attr("viewBox", "0 -5 10 10") | |
.attr("refX", 15) | |
.attr("refY", -1.5) | |
.attr("markerWidth", 6) | |
.attr("markerHeight", 6) | |
.attr("orient", "auto") | |
.append("svg:path") | |
.attr("d", "M0,-5L10,0L0,5"); | |
var path = svg.append("svg:g").selectAll("path") | |
.data(force.links()) | |
.enter().append("svg:path") | |
.attr("class", function(d) { return "link " + d.type; }) | |
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; }); | |
var node = svg.append("svg:g").selectAll(".node") | |
.data(force.nodes()) | |
.enter().append("g").attr("class", "node") | |
.call(force.drag); | |
node.append("image") | |
.attr("class", function (d) { return "group-" + d.group }) | |
.attr("xlink:href", function (d) { return d.icon }) | |
.attr("x", -8) | |
.attr("y", -8) | |
.attr("width", 16) | |
.attr("height", 16); | |
var text = svg.append("svg:g").selectAll("g") | |
.data(force.nodes()) | |
.enter().append("svg:g"); | |
// A copy of the text with a thick white stroke for legibility. | |
text.append("svg:text") | |
.attr("x", 8) | |
.attr("y", ".31em") | |
.attr("class", "shadow") | |
.text(function(d) { return d.name; }); | |
text.append("svg:text") | |
.attr("x", 8) | |
.attr("y", ".31em") | |
.text(function(d) { return d.name; }); | |
// Use elliptical arc path segments to doubly-encode directionality. | |
function tick() { | |
path.attr("d", function(d) { | |
var dx = d.target.x - d.source.x, | |
dy = d.target.y - d.source.y, | |
dr = Math.sqrt(dx * dx + dy * dy); | |
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; | |
}); | |
node.attr("transform", function(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}); | |
text.attr("transform", function(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}); | |
} | |
}); |
{ | |
"nodes": [ | |
{ | |
"group": 1, | |
"name": "Email", | |
"icon": "icon-envelope.png" | |
}, | |
{ | |
"group": 1, | |
"name": "IRC", | |
"icon": "icon-user.png" | |
}, | |
{ | |
"group": 1, | |
"name": "Android", | |
"icon": "icon-phone.png" | |
}, | |
{ | |
"group": 1, | |
"name": "Desktop", | |
"icon": "icon-desktop.png" | |
}, | |
{ | |
"group": 1, | |
"name": "Websockets", | |
"icon": "icon-websockets.png" | |
}, | |
{ | |
"group": 2, | |
"name": "remi", | |
"icon": "http://cdn.libravatar.org/avatar/33ddfefecc66c71eb7d6eb60503cf7f665d3bceb22e2f8e3af2704260a995ab2?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "spot", | |
"icon": "http://cdn.libravatar.org/avatar/1e232310ef80ec8b34b0bc216864efd0d837f419e6988997cda8e98a28be48dd?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "puiterwijk", | |
"icon": "http://cdn.libravatar.org/avatar/983782d075ab4e1fb02a7e7c7ca4d7096f6769bc9fe51b50e80eb012ddebc9ce?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "vicodan", | |
"icon": "http://cdn.libravatar.org/avatar/d16e1a49a16998d190f6bea5d0c36357d6e8bddc1427edb5ee277afabff16aee?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "pingou", | |
"icon": "http://cdn.libravatar.org/avatar/01fe73d687f4db328da1183f2a1b5b22962ca9d9c50f0728aafeac974856311c?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "mrunge", | |
"icon": "http://cdn.libravatar.org/avatar/c626775cb5584b1a49ea4a5cabb248e54e49c09185ebbf4fbce72265255956f0?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "orion", | |
"icon": "http://cdn.libravatar.org/avatar/b6fd449fcea49e2de27dacf226d46a01a0c8a44918672843e45bf5ac0bd0060b?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "cicku", | |
"icon": "http://cdn.libravatar.org/avatar/e04beb63c279ac05f99dd5ea1252e8d45b15611d1a9226bc42bda9fb31f24c75?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "rdieter", | |
"icon": "http://cdn.libravatar.org/avatar/f93140003949ff349627b7799edd42b927b057236ddd677b71257e0b974e6cb1?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "ralph", | |
"icon": "http://cdn.libravatar.org/avatar/9c9f7784935381befc302fe3c814f9136e7a33953d0318761669b8643f4df55c?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "pbrobinson", | |
"icon": "http://cdn.libravatar.org/avatar/861506b8dba9fe9ee12a83ce2cd7c6fff15091298f9f1a4e6149876490c53e9b?s=16" | |
}, | |
{ | |
"group": 2, | |
"name": "sgallagh", | |
"icon": "http://cdn.libravatar.org/avatar/0929fed032bd0a481ef74c46023fefe443f3d1b72dbe3efd293b25ed4fc843fd?s=16" | |
}, | |
{ | |
"group": 3, | |
"name": "Package Commits", | |
"icon": "https://apps.fedoraproject.org/img/icons/git-logo.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Wiki Edits", | |
"icon": "https://fedoraproject.org/w/skins/common/images/mediawiki.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Mailing List Messages", | |
"icon": "http://cloud.ohloh.net/attachments/37686/mailman_med.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Fedora Hosted Events", | |
"icon": "http://www.edgewall.org/gfx/trac_bullet.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Account Changes", | |
"icon": "https://admin.fedoraproject.org/accounts/static/theme/fas/images/account.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Koji Builds", | |
"icon": "http://fedoraproject.org/w/uploads/2/20/Artwork_DesignService_koji-icon-48.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Package Updates", | |
"icon": "https://admin.fedoraproject.org/updates/static/images/bodhi-icon-48.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Package ACL Updates", | |
"icon": "https://apps.fedoraproject.org/packages/images/icons/package_128x128.png" | |
}, | |
{ | |
"group": 3, | |
"name": "Calendar Events", | |
"icon": "https://apps.fedoraproject.org/calendar/static/calendar.png" | |
} | |
], | |
"links": [ | |
{ | |
"source": 1, | |
"type": "output", | |
"target": 11 | |
}, | |
{ | |
"source": 2, | |
"type": "output", | |
"target": 14 | |
}, | |
{ | |
"source": 3, | |
"type": "output", | |
"target": 10 | |
}, | |
{ | |
"source": 4, | |
"type": "output", | |
"target": 13 | |
}, | |
{ | |
"source": 0, | |
"type": "output", | |
"target": 9 | |
}, | |
{ | |
"source": 1, | |
"type": "output", | |
"target": 16 | |
}, | |
{ | |
"source": 2, | |
"type": "output", | |
"target": 7 | |
}, | |
{ | |
"source": 3, | |
"type": "output", | |
"target": 15 | |
}, | |
{ | |
"source": 4, | |
"type": "output", | |
"target": 6 | |
}, | |
{ | |
"source": 0, | |
"type": "output", | |
"target": 5 | |
}, | |
{ | |
"source": 1, | |
"type": "output", | |
"target": 8 | |
}, | |
{ | |
"source": 2, | |
"type": "output", | |
"target": 12 | |
}, | |
{ | |
"source": 3, | |
"type": "output", | |
"target": 11 | |
}, | |
{ | |
"source": 4, | |
"type": "output", | |
"target": 14 | |
}, | |
{ | |
"source": 0, | |
"type": "output", | |
"target": 10 | |
}, | |
{ | |
"source": 19, | |
"type": "input", | |
"target": 0 | |
}, | |
{ | |
"source": 25, | |
"type": "input", | |
"target": 1 | |
}, | |
{ | |
"source": 23, | |
"type": "input", | |
"target": 4 | |
}, | |
{ | |
"source": 22, | |
"type": "input", | |
"target": 3 | |
}, | |
{ | |
"source": 21, | |
"type": "input", | |
"target": 2 | |
}, | |
{ | |
"source": 24, | |
"type": "input", | |
"target": 0 | |
}, | |
{ | |
"source": 18, | |
"type": "input", | |
"target": 1 | |
}, | |
{ | |
"source": 20, | |
"type": "input", | |
"target": 4 | |
}, | |
{ | |
"source": 17, | |
"type": "input", | |
"target": 3 | |
}, | |
{ | |
"source": 19, | |
"type": "input", | |
"target": 2 | |
}, | |
{ | |
"source": 25, | |
"type": "input", | |
"target": 0 | |
}, | |
{ | |
"source": 23, | |
"type": "input", | |
"target": 1 | |
}, | |
{ | |
"source": 22, | |
"type": "input", | |
"target": 4 | |
} | |
] | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-type" content="text/html; charset=utf-8"> | |
<title>Fedora Notifications Dynamic Logo</title> | |
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script> | |
<link href="directed.css" type="text/css" media="all" rel="stylesheet"/> | |
</head> | |
<body> | |
<script type="text/javascript" src="directed.js"></script> | |
</body> | |
</html> | |
import requests | |
import hashlib | |
import random | |
import json | |
import fedmsg.config | |
import fedmsg.meta | |
config = fedmsg.config.load_config() | |
fedmsg.meta.make_processors(**config) | |
N_OUTPUT = 15 | |
N_INPUT = 13 | |
N_USERS = 12 | |
response = requests.get("https://badges.fedoraproject.org/leaderboard/json") | |
d = response.json() | |
usernames = [entry['nickname'] for entry in d['leaderboard']] | |
def libravatar(username): | |
template = "http://cdn.libravatar.org/avatar/{value}?s=16" | |
openid = "http://%s.id.fedoraproject.org/" % username | |
value = hashlib.sha256(openid).hexdigest() | |
return template.format(value=value) | |
# TODO these need icons | |
contexts = [ | |
{ | |
'name': ctx, | |
'group': 1, | |
'icon': icon, | |
} for ctx, icon in [ | |
('Email', 'icon-envelope.png'), | |
('IRC', 'icon-user.png'), | |
('Android', 'icon-phone.png'), | |
('Desktop', 'icon-desktop.png'), | |
('Websockets', 'icon-websockets.png'), | |
] | |
] | |
no_avatar = ['ausil', 'patches', 'nb', 'kalev'] | |
users = [ | |
{ | |
'name': username, | |
'group': 2, | |
'icon': libravatar(username), | |
} for username in usernames if username not in no_avatar] | |
sources = [ | |
{ | |
'name': proc.__obj__, | |
'icon': proc.__icon__, | |
'group': 3, | |
} for proc in fedmsg.meta.processors] | |
# TODO -- keep track of these to get more icons down the road. | |
# Clean out bogus ones. | |
for i in range(len(sources)): | |
if sources[i]['icon'] is None: | |
del sources[i]['icon'] | |
# Furthermore | |
for i in reversed(range(len(sources))): | |
if 'icon' not in sources[i]: | |
sources.pop(i) | |
#sources = random.sample(sources, 10) | |
users = random.sample(users, N_USERS) | |
nodes = contexts + users + sources | |
def make_random_links_1(N): | |
context_indices = range(len(contexts)) | |
user_indices = range(len(contexts), len(contexts) + len(users)) | |
random.shuffle(context_indices) | |
random.shuffle(user_indices) | |
for i in range(N): | |
yield { | |
'source': context_indices[i % len(context_indices)], | |
'target': user_indices[i % len(user_indices)], | |
'type': 'output', | |
} | |
def make_random_links_2(N): | |
context_indices = range(len(contexts)) | |
source_indices = range(len(contexts) + len(users), | |
len(contexts) + len(users) + len(sources)) | |
random.shuffle(context_indices) | |
random.shuffle(source_indices) | |
for i in range(N): | |
yield { | |
'source': source_indices[i % len(source_indices)], | |
'target': context_indices[i % len(context_indices)], | |
'type': 'input', | |
} | |
links = list(make_random_links_1(N_OUTPUT)) +\ | |
list(make_random_links_2(N_INPUT)) | |
print json.dumps(dict(nodes=nodes, links=links), indent=2) |