blob: e52bdd7c772fa4c917d61b6b309428b20258b74d [file] [log] [blame]
Scott Baker4ee5b6d2014-03-27 09:17:59 -07001<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
2<style>
3#slice_interaction_chart_placeholder {
4 text-align: center;
5 margin: -40px 20px 20px 0px;
6 color:#fff;
7 position: relative;
8 height: 100%;
9 width: 100%;
10}
11.dependencyWheel {
12 font: 10px sans-serif;
13}
14form .btn-primary {
15 margin-top: 25px;
16}
17.labeltext {
18 color: #fff;
19}
20#circle circle {
21 fill: none;
22 pointer-events: all;
23}
24path.chord {
25 stroke: #000;
26 stroke-width: .10px;
27 transition: opacity 0.3s;
28}
29#circle:hover path.fade {
30 opacity: 0;
31}
32a {
33 text-decoration: none;
34 border-bottom: 1px dotted #666;
35 color: #999;
36}
37.more a {
38 color: #666;
39}
40.by a {
41 color: #fff;
42}
43a:hover {
44 color: #45b8e2;
45}
46a:not(:hover) {
47 text-decoration: none;
48}
49text {
50 fill: black;
51}
52svg {
53 font-size: 12px;
54 font-weight: bold;
55 color: #999;
56 font-family:'Arial', sans-serif;
57 min-height: 100%;
58 min-width: 100%;
59}
60button:disabled {
61 color:red;
62 background-color: lightyellow;
63}
64#sliceEngagementTitle {
65 margin-top: -50px;
66 margin-left: -40px;
67
68}
69
70</style>
71<h3 id="sliceEngagementTitle"> Involvement between Slices by User Engagement</h3>
72<div id="slice_interaction_chart_placeholder"></div>
73
74<script>
75
76 /* d3.json(datasetURL, function(error, matrix) {
77
78 if (error) {alert("Error reading file: ", error.statusText); return; }
79
80 */
81 actualData = [[2, 2, 2, 2, 2, 2, 2, 1, 2, 1],
82 [2, 7, 3, 7, 7, 3, 2, 2, 7, 2],
83 [2, 3, 4, 3, 4, 2, 2, 2, 3, 2],
84 [2, 7, 3, 7, 7, 3, 2, 2, 7, 2],
85 [2, 7, 4, 7, 15, 3, 2, 6, 7, 6],
86 [2, 3, 2, 3, 3, 3, 2, 1, 3, 1],
87 [2, 2, 2, 2, 2, 2, 2, 1, 2, 1],
88 [1, 2, 2, 2, 6, 1, 1, 6, 2, 6],
89 [2, 7, 3, 7, 7, 3, 2, 2, 7, 2],
90 [1, 2, 2, 2, 6, 1, 1, 6, 2, 6]];
91
92// Chord Diagram for showing Collaboration between users found in an anchor query
93// Collaboration View
94//
95
96var width = 600,
97 height = 600,
98 outerRadius = Math.min(width, height) / 2 - 100,
99 innerRadius = outerRadius - 18;
100
101var dataset = "#allinfo";
102//string url for the initial data set
103//would usually be a file path url, here it is the id
104//selector for the <pre> element storing the data
105
106//create number formatting functions
107var formatPercent = d3.format("%");
108var numberWithCommas = d3.format("0,f");
109
110//create the arc path data generator for the groups
111var arc = d3.svg.arc()
112 .innerRadius(innerRadius)
113 .outerRadius(outerRadius);
114
115//create the chord path data generator for the chords
116var path = d3.svg.chord()
117 .radius(innerRadius);
118
119//define the default chord layout parameters
120//within a function that returns a new layout object;
121//that way, you can create multiple chord layouts
122//that are the same except for the data.
123function getDefaultLayout() {
124 return d3.layout.chord()
125// .padding(0.03)
126 .sortSubgroups(d3.descending)
127 .sortChords(d3.ascending);
128}
129var last_layout; //store layout between updates
130var users = [{"color": "#005586", "name": "Owl"}, {"color": "#6ebe49", "name": "DnsDemux"}, {"color": "orange", "name": "Infrastructure"}, {"color": "#707170", "name": "HyperCache"}, {"color": "#00c4b3", "name": "Syndicate"}, {"color": "#077767", "name": "Hadoop"}, {"color": "dodgerblue", "name": "Stork"}, {"color": "#a79b94", "name": "test2"}, {"color": "#c4e76a", "name": "DnsRedir"}, {"color": "red", "name": "test"}];
131
132/*** Initialize the visualization ***/
133var g = d3.select("#slice_interaction_chart_placeholder").append("svg")
134 .attr("width", width)
135 .attr("height", height)
136 .append("g")
137 .attr("id", "circle")
138 .attr("transform",
139 "translate(" + width / 2 + "," + height / 2 + ")");
140//the entire graphic will be drawn within this <g> element,
141//so all coordinates will be relative to the center of the circle
142
143g.append("circle")
144 .attr("r", outerRadius);
145//this circle is set in CSS to be transparent but to respond to mouse events
146//It will ensure that the <g> responds to all mouse events within
147//the area, even after chords are faded out.
148
149/*** Read in the neighbourhoods data and update with initial data matrix ***/
150//normally this would be done with file-reading functions
151//d3.csv and d3.json and callbacks,
152//instead we're using the string-parsing functions
153//d3.csv.parse and JSON.parse, both of which return the data,
154//no callbacks required.
155
156
157 updateChords(dataset);
158 //call the update method with the default dataset
159
160//} ); //end of d3.csv function
161
162
163/* Create OR update a chord layout from a data matrix */
164function updateChords( datasetURL ) {
165
166 /* d3.json(datasetURL, function(error, matrix) {
167
168 if (error) {alert("Error reading file: ", error.statusText); return; }
169
170 */
171 //var matrix = JSON.parse( d3.select(datasetURL).text() );
172 var matrix = actualData;
173 // instead of d3.json
174
175 /* Compute chord layout. */
176 layout = getDefaultLayout(); //create a new layout object
177 layout.matrix(matrix);
178
179 /* Create/update "group" elements */
180 var groupG = g.selectAll("g.group")
181 .data(layout.groups(), function (d) {
182 return d.index;
183 //use a key function in case the
184 //groups are sorted differently between updates
185 });
186
187 groupG.exit()
188 .transition()
189 .duration(1500)
190 .attr("opacity", 0)
191 .remove(); //remove after transitions are complete
192
193 var newGroups = groupG.enter().append("g")
194 .attr("class", "group");
195 //the enter selection is stored in a variable so we can
196 //enter the <path>, <text>, and <title> elements as well
197
198
199 //Create the title tooltip for the new groups
200 newGroups.append("title");
201
202 //Update the (tooltip) title text based on the data
203 groupG.select("title")
204 .text(function(d, i) {
205 return "Slice (" + users[i].name +
206 ") "
207 ;
208 });
209
210 //create the arc paths and set the constant attributes
211 //(those based on the group index, not on the value)
212 newGroups.append("path")
213 .attr("id", function (d) {
214 return "group" + d.index;
215 //using d.index and not i to maintain consistency
216 //even if groups are sorted
217 })
218 .style("fill", function (d) {
219 return users[d.index].color;
220 });
221
222 //update the paths to match the layout
223 groupG.select("path")
224 .transition()
225 .duration(1500)
226 .attr("opacity", 0.5) //optional, just to observe the transition
227 .attrTween("d", arcTween( last_layout ))
228 // .transition().duration(100).attr("opacity", 1) //reset opacity
229 ;
230
231 //create the group labels
232 newGroups.append("svg:text")
233 .attr("xlink:href", function (d) {
234 return "#group" + d.index;
235 })
236 .attr("dy", ".35em")
237 .attr("color", "#fff")
238 .text(function (d) {
239 return users[d.index].name;
240 });
241
242 //position group labels to match layout
243 groupG.select("text")
244 .transition()
245 .duration(1500)
246 .attr("transform", function(d) {
247 d.angle = (d.startAngle + d.endAngle) / 2;
248 //store the midpoint angle in the data object
249
250 return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
251 " translate(" + (innerRadius + 26) + ")" +
252 (d.angle > Math.PI ? " rotate(180)" : " rotate(0)");
253 //include the rotate zero so that transforms can be interpolated
254 })
255 .attr("text-anchor", function (d) {
256 return d.angle > Math.PI ? "end" : "begin";
257 });
258
259
260 /* Create/update the chord paths */
261 var chordPaths = g.selectAll("path.chord")
262 .data(layout.chords(), chordKey );
263 //specify a key function to match chords
264 //between updates
265
266
267 //create the new chord paths
268 var newChords = chordPaths.enter()
269 .append("path")
270 .attr("class", "chord");
271
272 // Add title tooltip for each new chord.
273 newChords.append("title");
274
275 // Update all chord title texts
276 chordPaths.select("title")
277 .text(function(d) {
278 if (users[d.target.index].name !== users[d.source.index].name) {
279 return [numberWithCommas(d.source.value),
280 " users in common between \n",
281 users[d.source.index].name,
282 " and ",
283 users[d.target.index].name,
284 "\n"
285 ].join("");
286 //joining an array of many strings is faster than
287 //repeated calls to the '+' operator,
288 //and makes for neater code!
289 }
290 else { //source and target are the same
291 return numberWithCommas(d.source.value)
292 + " users are only in Slice ("
293 + users[d.source.index].name + ")";
294 }
295 });
296
297 //handle exiting paths:
298 chordPaths.exit().transition()
299 .duration(1500)
300 .attr("opacity", 0)
301 .remove();
302
303 //update the path shape
304 chordPaths.transition()
305 .duration(1500)
306 //.attr("opacity", 0.5) //optional, just to observe the transition
307 .style("fill", function (d) {
308 return users[d.source.index].color;
309 })
310 .attrTween("d", chordTween(last_layout))
311 //.transition().duration(100).attr("opacity", 1) //reset opacity
312 ;
313
314 //add the mouseover/fade out behaviour to the groups
315 //this is reset on every update, so it will use the latest
316 //chordPaths selection
317 groupG.on("mouseover", function(d) {
318 chordPaths.classed("fade", function (p) {
319 //returns true if *neither* the source or target of the chord
320 //matches the group that has been moused-over
321 return ((p.source.index != d.index) && (p.target.index != d.index));
322 });
323 });
324 //the "unfade" is handled with CSS :hover class on g#circle
325 //you could also do it using a mouseout event:
326 /*
327 g.on("mouseout", function() {
328 if (this == g.node() )
329 //only respond to mouseout of the entire circle
330 //not mouseout events for sub-components
331 chordPaths.classed("fade", false);
332 });
333 */
334
335 last_layout = layout; //save for next update
336
337// }); //end of d3.json
338}
339
340function arcTween(oldLayout) {
341 //this function will be called once per update cycle
342
343 //Create a key:value version of the old layout's groups array
344 //so we can easily find the matching group
345 //even if the group index values don't match the array index
346 //(because of sorting)
347 var oldGroups = {};
348 if (oldLayout) {
349 oldLayout.groups().forEach( function(groupData) {
350 oldGroups[ groupData.index ] = groupData;
351 });
352 }
353
354 return function (d, i) {
355 var tween;
356 var old = oldGroups[d.index];
357 if (old) { //there's a matching old group
358 tween = d3.interpolate(old, d);
359 }
360 else {
361 //create a zero-width arc object
362 var emptyArc = {startAngle:d.startAngle,
363 endAngle:d.startAngle};
364 tween = d3.interpolate(emptyArc, d);
365 }
366
367 return function (t) {
368 return arc( tween(t) );
369 };
370 };
371}
372
373function chordKey(data) {
374 return (data.source.index < data.target.index) ?
375 data.source.index + "-" + data.target.index:
376 data.target.index + "-" + data.source.index;
377
378 //create a key that will represent the relationship
379 //between these two groups *regardless*
380 //of which group is called 'source' and which 'target'
381}
382function chordTween(oldLayout) {
383 //this function will be called once per update cycle
384
385 //Create a key:value version of the old layout's chords array
386 //so we can easily find the matching chord
387 //(which may not have a matching index)
388
389 var oldChords = {};
390
391 if (oldLayout) {
392 oldLayout.chords().forEach( function(chordData) {
393 oldChords[ chordKey(chordData) ] = chordData;
394 });
395 }
396
397 return function (d, i) {
398 //this function will be called for each active chord
399
400 var tween;
401 var old = oldChords[ chordKey(d) ];
402 if (old) {
403 //old is not undefined, i.e.
404 //there is a matching old chord value
405
406 //check whether source and target have been switched:
407 if (d.source.index != old.source.index ){
408 //swap source and target to match the new data
409 old = {
410 source: old.target,
411 target: old.source
412 };
413 }
414
415 tween = d3.interpolate(old, d);
416 }
417 else {
418 //create a zero-width chord object
419 if (oldLayout) {
420 var oldGroups = oldLayout.groups().filter(function(group) {
421 return ( (group.index == d.source.index) ||
422 (group.index == d.target.index) )
423 });
424 old = {source:oldGroups[0],
425 target:oldGroups[1] || oldGroups[0] };
426 //the OR in target is in case source and target are equal
427 //in the data, in which case only one group will pass the
428 //filter function
429
430 if (d.source.index != old.source.index ){
431 //swap source and target to match the new data
432 old = {
433 source: old.target,
434 target: old.source
435 };
436 }
437 }
438 else old = d;
439
440 var emptyChord = {
441 source: { startAngle: old.source.startAngle,
442 endAngle: old.source.startAngle},
443 target: { startAngle: old.target.startAngle,
444 endAngle: old.target.startAngle}
445 };
446 tween = d3.interpolate( emptyChord, d );
447 }
448
449 return function (t) {
450 //this function calculates the intermediary shapes
451 return path(tween(t));
452 };
453 };
454}
455
456
457/* Activate the buttons and link to data sets */
458d3.select("#ReadersButton").on("click", function () {
459 updateChords( "#readinfo" );
460 //replace this with a file url as appropriate
461
462 //enable other buttons, disable this one
463 disableButton(this);
464});
465
466d3.select("#ContributorsButton").on("click", function() {
467 updateChords( "#contributorinfo" );
468 disableButton(this);
469});
470
471d3.select("#AllUsersButton").on("click", function() {
472 updateChords( "#allinfo" );
473 disableButton(this);
474});
475function disableButton(buttonNode) {
476 d3.selectAll("button")
477 .attr("disabled", function(d) {
478 return this === buttonNode? "true": null;
479 });
480}
481
482</script>