Skip to content

Commit 5313410

Browse files
authored
Merge pull request #614 from artoonie/decimate-sankey
decimate sankey data (~1/10th the JS for large elections)
2 parents 59788d9 + d4bbce6 commit 5313410

3 files changed

Lines changed: 60 additions & 28 deletions

File tree

static/sankey/sankey-wrapper.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
function decompressGraph(c, charMap) {
2+
// charMap: single char -> candidate name, ordered by elimination order.
3+
// A char's position (charCode - 'a') is its color index.
4+
const nodes = c.nodes.map(([ch, round, value, w, e]) => ({
5+
name: charMap[ch],
6+
round,
7+
value,
8+
isWinner: w,
9+
isEliminated: e,
10+
index: ch.charCodeAt(0) - 97,
11+
}));
12+
const links = c.links.map(([source, target, value]) => ({
13+
source,
14+
target,
15+
candidateIndex: nodes[source].index,
16+
value,
17+
}));
18+
return { nodes, links };
19+
}
20+
121
function makeSankey(graph, numRounds, numCandidates, numWinners, longestLabelApxWidth, totalVotesPerRound, colorThemeIndex) {
222
// Below are crazy heuristics to try to get the graph to look good
323
// on a variety of sizes.

templates/sankey/sankey-nonblocking.html

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@
1414
<script type="text/javascript">
1515
{{ sankeyjs|safe }} // lgtm [js/useless-expression]
1616

17-
if (numRounds > 1)
18-
{
19-
loadFunctions();
20-
makeSankey(graph, numRounds, numCandidates, numWinners, longestLabelApxWidth, totalVotesPerRound, config.colorTheme);
21-
}
22-
else
23-
{
24-
d3.select("#sankey-body").append("text")
25-
.text("Sankey diagrams show a flow from one round to the next. This single-round election cannot be displayed as a Sankey diagram.")
26-
.style("margin-left", "50px")
17+
function renderSankey() {
18+
const graph = decompressGraph(graphCompressed, charMap);
19+
if (numRounds > 1)
20+
{
21+
loadFunctions();
22+
makeSankey(graph, numRounds, numCandidates, numWinners, longestLabelApxWidth, totalVotesPerRound, config.colorTheme);
23+
}
24+
else
25+
{
26+
d3.select("#sankey-body").append("text")
27+
.text("Sankey diagrams show a flow from one round to the next. This single-round election cannot be displayed as a Sankey diagram.")
28+
.style("margin-left", "50px")
29+
}
2730
}
31+
32+
requestAnimationFrame(renderSankey);
2833
</script>

visualizer/sankey/graphToD3.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,42 @@ def __init__(self, graph):
1414
js += f'numWinners = {graph.summarize().numWinners} ;\n'
1515
js += f'longestLabelApxWidth = {longestLabelApxWidth};\n'
1616
js += f'totalVotesPerRound = {totalVotesPerRound};\n'
17-
js += 'graph = {"nodes" : [], "links" : []};\n'
18-
19-
# Maps Candidates to a unique index. Used for color indexing.
20-
indices = {candidate: i for i, candidate in enumerate(graph.eliminationOrder)}
17+
# Maps Candidates to a single char key (a-z) ordered by elimination order.
18+
# Position in charMap doubles as the color index. Supports up to 26 candidates.
19+
elimination_order = list(graph.eliminationOrder)
20+
candidate_to_char = {c: chr(ord('a') + i) for i, c in enumerate(elimination_order)}
21+
char_map = {chr(ord('a') + i): c.name for i, c in enumerate(elimination_order)}
2122

2223
nodeIndices = {}
23-
for i, node in enumerate(graph.nodes):
24+
nodes = []
25+
for node in graph.nodes:
2426
# Skip inactive (exhausted) nodes
2527
if not node.candidate.isActive:
2628
continue
2729

28-
nodeIndices[node] = i
29-
js += f'graph.nodes.push({{ "name": {json.dumps(node.label)},\n'
30-
js += f' "round": {node.roundNum},\n'
31-
js += f' "value": {node.count},\n'
32-
js += f' "isWinner": {int(node.isWinner)},\n'
33-
js += f' "isEliminated": {int(node.isEliminated)},\n'
34-
js += f' "index": "{indices[node.candidate]}"}});\n'
30+
nodeIndices[node] = len(nodes)
31+
nodes.append([
32+
candidate_to_char[node.candidate],
33+
node.roundNum,
34+
node.count,
35+
int(node.isWinner),
36+
int(node.isEliminated),
37+
])
38+
39+
links = []
3540
for link in graph.links:
3641
# Skip inactive (exhausted) nodes
3742
if not link.source.candidate.isActive:
3843
continue
3944
if not link.target.candidate.isActive:
4045
continue
4146

42-
sourceIndex = nodeIndices[link.source]
43-
targetIndex = nodeIndices[link.target]
44-
js += f'graph.links.push({{ "source": {sourceIndex},\n'
45-
js += f' "target": {targetIndex},\n'
46-
js += f' "candidateIndex": {indices[link.source.candidate]},\n'
47-
js += f' "value": {link.value:0.3f} }});\n'
47+
links.append([
48+
nodeIndices[link.source],
49+
nodeIndices[link.target],
50+
round(link.value, 3),
51+
])
52+
53+
js += f'charMap = {json.dumps(char_map)};\n'
54+
js += f'graphCompressed = {json.dumps({"nodes": nodes, "links": links})};\n'
4855
self.js = js

0 commit comments

Comments
 (0)