-
Notifications
You must be signed in to change notification settings - Fork 5
/
tutor-tree.js
304 lines (249 loc) · 8.6 KB
/
tutor-tree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
queue()
.defer(d3.csv, 'NCSS2007.csv')
.defer(d3.csv, 'NCSS2008.csv')
.defer(d3.csv, 'NCSS2009.csv')
.defer(d3.csv, 'NCSS2010.csv')
.defer(d3.csv, 'NCSS2011.csv')
.defer(d3.csv, 'NCSS2012.csv')
.defer(d3.csv, 'NCSS2013.csv')
.defer(d3.csv, 'NCSS2014.csv')
.await(loadNCSSTree)
// A list of people's names
var names = d3.map();
// A map of the tutors by group and year
var groupTutors = d3.map();
// Relationships list
var relationships = [];
function loadNCSSTree(error) {
// Take in all the trees from the arguments
var trees = [];
for (var i = 1; i < arguments.length; i++)
trees.push(arguments[i]);
// Build the groupTutors map
trees.forEach(function(tree, year) {
year += 2009;
// Add a map for each year's groups
groupTutors.set(year, d3.map());
tree.forEach(function(person) {
if (isTutor(person)) {
// Get the group of tutors this person is in
var group = groupTutors.get(year).get(person.group);
// Construct a new group if it doesn't exist yet
if (group === undefined)
group = groupTutors.get(year).set(person.group, []);
// Add the person to the group
group.push(person);
}
// Add the person to the set of people
names.set(person.name, {name: person.name});
});
});
console.log(groupTutors);
// Build the tutoredBy and tutoredWith maps
trees.forEach(function(tree, year) {
year += 2009;
tree.forEach(function(person) {
// Build the tutoredWith map
if (isTutor(person)) {
// Get the tutors this person tutored with
var fellowTutors = groupTutors.get(year).get(person.group);
// Add relationships
if (fellowTutors)
fellowTutors.forEach(function(tutor) {
relationships.push({
source: names.get(person.name),
target: names.get(tutor.name),
relationship: 'tutored with',
year: year,
});
});
}
// Build the tutoredBy map
else if (person.role === "student") {
// Get the tutors who tutored this person
var tutors = groupTutors.get(year).get(person.group);
// Add relationships
if (tutors)
tutors.forEach(function(tutor) {
relationships.push({
source: names.get(person.name),
target: names.get(tutor.name),
relationship: 'tutored by',
year: year,
});
});
}
});
});
// Create the graph visualization
var relFilter = d3.map();
relFilter.set('tutored by', true);
var personRadius = 20;
// Create a colour set for the graph
colours = []
for (var hue = 0; hue < 360; hue += 50)
colours.push('hsla(' + hue + ', 60%, 70%, 0.8)');
var relationshipColour = d3.scale.ordinal()
.range(colours);
// SVG element
var svg = d3.select('.graph').append('svg');
// Scaling
x = d3.scale.linear()
.range([0, svg.property('offsetWidth')]);
xScale = d3.scale.linear()
.range([0, 1]);
y = d3.scale.linear()
.range([0, svg.property('offsetHeight')]);
yScale = d3.scale.linear()
.range([0, 1]);
// Zoom behaviour
function peopleTransform(person) {
people.attr("transform", function(person) {
return "translate(" + x(xScale(person.layout.x)) + "," + y(yScale(person.layout.y)) + ")";
});
}
function relsTransform() {
rels.attr('xxxx', function(d) { return JSON.stringify(d.source.layout) } );
rels
.attr("x1", function(d) { return x(xScale(d.source.layout.x)); })
.attr("y1", function(d) { return y(yScale(d.source.layout.y)); })
.attr("x2", function(d) { return x(xScale(d.target.layout.x)); })
.attr("y2", function(d) { return y(yScale(d.target.layout.y)); });
}
function transformGraph() {
peopleTransform();
relsTransform();
}
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.on("zoom", transformGraph)
svg.call(zoom);
// Groups of elements
var container = svg.append('svg:g')
var relationshipGroup = container.append('svg:g')
.attr('class', 'relationships');
var peopleGroup = container.append('svg:g')
.attr('class', 'people');
var people, rels;
updateGraph = function() {
// Filter by relationship type filter
var relations = relationships.filter(function(rel) {
return relFilter.get(rel.relationship);
});
// Filter people by relations
var peeps = names.values().filter(function(person) {
return relations.filter(function(rel) { return rel.source.name == person.name || rel.target.name == person.name; }).length > 0;
});
// Each person has an group element
people = peopleGroup.selectAll('g.person')
.data(peeps);
// People get given a group element when they start NCSS
group = people.enter().append('svg:g')
.attr('class', 'person');
// ...with a circle
group.append('svg:circle')
.attr('r', personRadius)
// ...and a label for their name
group.append('svg:text')
.attr('fill', 'white')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
// People's elements are positioned
people
.attr('title', function(person) { return person.name; })
.each(function(person) {
// ...and are labelled with their names
d3.select(this).select('text')
.text(function(person) { return person.name; });
});
// The bubbles are fit to the text
group.selectAll('text').each(function() {
var bbox = this.getBBox();
if (bbox.width > personRadius)
personRadius = bbox.width/2 + 20;
});
// People's circles are resized
people
.each(function(person) {
d3.select(this).select('circle')
.attr('r', personRadius);
});
people.exit().remove();
// People have relationships
rels = relationshipGroup.selectAll('line.relationship')
.data(relations);
// Relationships are given lines
rels.enter().append('svg:line')
rels.exit().remove();
// Relationships are classed, coloured and labelled
rels
.attr('class', function(rel) { return 'relationship ' + rel.relationship; })
.attr('stroke', function(rel) { return relationshipColour(rel.relationship); })
.attr('source', function(rel) { return rel.source.name; })
.attr('target', function(rel) { return rel.target.name; })
// Layout the graph
var graph = new dagre.Digraph();
// Add the nodes
peeps.forEach(function(person) {
graph.addNode(person.name, {width: personRadius*2, height: personRadius*2});
})
// Add the edges
relations.forEach(function(rel) {
graph.addEdge(null, rel.source.name, rel.target.name);
});
// Calculate the layout
var layout = dagre.layout()
.nodeSep(personRadius*2)
.rankDir("TB")
.run(graph);
var maxx = 0, maxy = 0, minx = Infinity, miny = Infinity;
layout.eachNode(function(name, layout) {
names.get(name).layout = layout;
maxx = Math.max(maxx, layout.x);
maxy = Math.max(maxy, layout.y);
minx = Math.min(minx, layout.x);
miny = Math.min(miny, layout.y);
});
// Normalize the node positions
zoom.translate([0,0]);
zoom.size(1);
xScale.domain([minx-personRadius, maxx+personRadius]);
yScale.domain([miny-personRadius, maxy+personRadius]);
// Apply the layout
transformGraph();
}
updateGraph();
// Get the set of all relationship types
var relTypes = d3.set(relationships.map(function(rel) { return rel.relationship}))
// Create a legend
var legend = d3.select('.legend-section').append('ul');
legend
.attr('class', 'legend');
// Add the entries
var entries = legend.selectAll('li')
.data(relTypes.values());
// Style entries
entries.enter().append('li')
.each(function(relType) {
// Checkbox for hiding/showing relationships in the graph
d3.select(this).append('input')
.attr('type', 'checkbox')
.property('checked', function(d) { return relFilter.get(d) || false; })
.on('click', function(relType) {
relFilter.set(relType, this.checked);
updateGraph();
})
// Colour key for identifying the relationship
d3.select(this).append('span')
.attr('class', 'colour-key')
.style('background', relationshipColour(relType));
// Name of the type of the relationship
d3.select(this).append('span')
.text(function(relType) { return relType; });
})
}
// Determine whether a person's role is a tutor role
function isTutor(person) {
return person.role === "tutor" || person.role === "industry tutor" || person.role === "group leader";
}