This repository has been archived by the owner on Oct 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
mhv_spaceprobe_vis.js
417 lines (366 loc) · 14.7 KB
/
mhv_spaceprobe_vis.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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/*
Using jTwitter.js to grab tweets from the 'makehackvoid' twitter account and then
select the ones that came from the spaceprobe.
Using d3.js to display visualisations of the open / close / extend events
Possible formats from the spaceprobe are:
The MHV space will remain open for approximately another xxxx hours (~hh:mm)
The MHV space will remain open for approximately another nn minutes (~18:15)
The MHV space is now open for approximately xxxx hours (~hh:mm)
The MHV space is now open for approximately an hour (~hh:mm)
The MHV space is now closed (was open 4 1/2 hours)
The MHV space is now closed (was open nnn minutes)
The MHV space is now closed (was open xxxx hours)
(~hh:mm) is the estimated closing time
Note: all time estimates in brackets seem to be rounded to nearest 15 minutes
Format of tweets changed on around 20 November 2011. New format:
Space is closed (was open 10 hours)
Space is closed (was open 159 minutes)
Space is open until hh:mm (estimate)
Space staying open until hh:mm (estimate)
Space Probe here. It gets lonely in this empty space sometimes. Come and keep me company.
Ideas to try:
1. create datapoints based on open / close pairs
for example open is terminated by close, or if a close isn't seen, by the
subsequent open and just uses predicted open + any extensions.
2. Change x axis to be dates, not days
3. transition so data appears gradually onto graph (and maybe older data fades out?)
way to do this is given in http://mbostock.github.com/d3/tutorial/bar-2.html
4. Possibly try a spiral display of the timeline
*/
var outputFormat = d3.time.format("%Y %m %d %H:%M:%S");
// Note: d3 doesn't support %Z in input parsing yet
// var createdAtFormat = d3.time.format("%a %b %d %H:%M:%S %Z %Y");
var newData = new Array();
$(document).ready(function() {
// Get latest tweets using jTwitter
$.jTwitter('makehackvoid', 500, function(data) {
$('#posts').empty();
$('#posts').append('<button>Show data table</button>');
$('#posts').append('<table id="tweetsTable">');
$('#tweetsTable')
.append('<tr><th>Time</th><th>Tweet text</th><th>parsed times (close time / actual open period)</th></tr>');
$.each(data, function(i, post) {
// only look at tweets that say they came from the Space Probe
if (post.source.indexOf("MHV Space Probe") != -1) {
var tweetType = "";
var tweetDate = new Date(post.created_at);
var actualOpenPeriod = "";
var closeTimeEstimate = "";
textDate = outputFormat(tweetDate);
if (post.text.indexOf("is now open") != -1) {
tweetType = "o"; // open
startEstimate = post.text.indexOf("(~");
endEstimate = post.text.indexOf(")");
if (startEstimate != -1 && endEstimate != -1 && startEstimate < endEstimate) {
closeTimeEstimate = post.text.substring(startEstimate+2, endEstimate);
}
} else if (post.text.indexOf('is open') != -1) {
tweetType = "o"; // open
startEstimate = post.text.indexOf("until ");
endEstimate = post.text.indexOf(" (estimate)");
if (startEstimate != -1 && endEstimate != -1 && startEstimate < endEstimate) {
closeTimeEstimate = post.text.substring(startEstimate+6, endEstimate);
}
} else if (post.text.indexOf("is now closed") != -1 || post.text.indexOf("is closed") != -1) {
tweetType = "c"; // closed
startOpen = post.text.indexOf("(was open ");
endOpen = post.text.indexOf(")");
if (startOpen != -1 && endOpen != -1 && startOpen < endOpen) {
actualOpenPeriod = post.text.substring(startOpen+("(was open ").length, endOpen);
}
} else if (post.text.indexOf("will remain open") != -1 || post.text.indexOf("staying open") != -1) {
tweetType = "e"; // extend opening time
}
if (tweetType == "o" || tweetType == "c") {
// ignoring extensions for now
if (actualOpenPeriod.indexOf("days") == -1 ) {
// if actual open period contains "days" discard it as out of range
// display the tweets & parsed data
$('#tweetsTable').append(
'<tr>'
+' <td>'
+ tweetDate
+' </td>'
+' <td>'
+ post.text
+' </td>'
+' <td>'
+ closeTimeEstimate + " / " + actualOpenPeriod
+' </td>'
+'</tr>'
);
var mhv = new Object();
mhv.tweetDate = tweetDate;
mhv.tweetType = tweetType;
mhv.openEstimate = closeTimeEstimate;
mhv.actualOpen = actualOpenPeriod;
newData.push(mhv);
}
}
}
});
$('#posts').append('</table>');
drawGraph(newData);
});
});
function drawGraph(data) {
// function to draw the graph
// second version with lots of help from http://www.recursion.org/d3-for-mere-mortals/
// Thanks @lof for your d3.js timeline tutorial, I'm finally starting to understand how it works.
var w = 640,
h = 380,
padding = 50;
// define the y scale (note: need to coerce data to 1.1.2011 before scaling)
var y = d3.time.scale()
.domain([new Date(2011, 0, 1), new Date(2011, 0, 1, 23, 59)])
.range([0, h]);
// define x scale (days of week in current version)
// var x = d3.time.scale().domain([new Date(2011, 0, 1), new Date(2011, 11, 31)]).range([0, width]);
var monthNames = ["Jan", "Feb", "Mar", "April", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var hourNames = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"];
var x = d3.scale.linear()
// .domain([0, data.length])
.domain([1, 7])
.range([padding/2, w - padding/2]);
function yAxisLabel(d) {
if (d == 12) {
return "noon";
}
if (d < 12) {
return d;
}
return (d - 12);
}
function midMonthDates() {
// The labels along the x axis will be positioned on the 15th of the month
return d3.range(0, 12).map(function(i) {
return new Date(2011, i, 15)
});
}
function midDayPos() {
// day labels for x axis
return d3.range(1, 8).map(function(i) {
return i
});
}
// create the chart
var daychart =
d3.select("#daybreakdown").append("svg:svg")
.attr("class", "daychart")
.attr("width", w + padding * 2)
.attr("height", h + padding * 2);
// create a group to hold the axis-related elements
var axisGroup = daychart
.append("svg:g")
.attr("transform", "translate(" + padding + "," + padding + ")");
// add the chart axis to the axisGroup
axisGroup.selectAll(".yTicks")
.data(d3.range(0, 24))
.enter().append("svg:line")
.attr("x1", -5)
// Round and add 0.5 to fix anti-aliasing effects
.attr("y1", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5;})
.attr("x2", w + 5)
.attr("y2", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5;})
.attr("class", "yTicks");
axisGroup.selectAll(".xTicks")
//.data(midMonthDates)
.data(midDayPos)
.enter().append("svg:line")
.attr("x1", x)
.attr("y1", -5)
.attr("x2", x)
.attr("y2", h + 5)
.attr("class", "xTicks");
// draw the text for the labels
axisGroup.selectAll("text.xAxisTop")
// .data(midMonthDates)
.data(midDayPos)
.enter().append("svg:text")
.text(function(d, i) { return dayNames[i];})
.attr("x", x)
.attr("y", -8)
.attr("text-anchor", "middle")
.attr("class", "axis xAxisTop");
axisGroup.selectAll("text.yAxisLeft")
.data(d3.range(0, 24))
.enter().append("svg:text")
.text(yAxisLabel)
.attr("x", -7)
.attr("y", function(d) { return y(new Date(2011, 0, 1, d)); })
.attr("dy", "3")
.attr("class", "axis yAxisLeft")
.attr("text-anchor", "end");
var graphGroup = daychart.append("svg:g")
.attr("transform", "translate(" + padding + ", " + padding + ")");
// Circles to show the events
var circle = graphGroup.selectAll("circle")
.data(data);
circle.enter().append("svg:circle")
.attr("cy", function(d) { return y(new Date(2011, 0, 1, d.tweetDate.getHours(), d.tweetDate.getMinutes())); })
.attr("cx", function(d) { return x(d.tweetDate.getDay() + 1); })
.attr("r", 10)
.attr("class", function(d) { return "circle_" + d.tweetType; });
circle.exit().remove();
// Symbols to show the events
var path = graphGroup.selectAll("path")
.data(data);
path.enter().append("svg:path")
.attr("transform", function(d) {
return "translate("
+ x(d.tweetDate.getDay() + 1) + ","
+ y(new Date(2011, 0, 1, d.tweetDate.getHours(),d.tweetDate.getMinutes()))
+ ")"; })
.attr("d", d3.svg.symbol().type(function(d) {return symType(d.tweetType);}).size(20))
.attr("class", function(d) { return "symbol_" + d.tweetType; });
function symType(tweetType) {
var symType = "circle";
switch (tweetType)
{
case "c":
symType = "triangle-up";
break;
case "o":
symType = "triangle-down"
break;
case "e":
symType = "square";
}
return(symType);
}
// Rectangles to show the events
var rectangle = graphGroup.selectAll("rect")
.data(data);
var barWidth = 10;
rectangle.enter().append("svg:rect")
.attr("x", function(d) { return x(d.tweetDate.getDay() + 1) - barWidth/2; })
.attr("y", function(d) { return y(new Date(2011, 0, 1, d.tweetDate.getHours(), d.tweetDate.getMinutes())); })
.attr("width", barWidth)
.attr("height", function(d) { return barHeight(d); })
.attr("class", function(d) { return "rect_" + d.tweetType; })
// transform close bars to be above the close time, and if they are up to 2am, move them to end of previous day
.attr("transform", function(d) { return transXY(d)});
rectangle.exit().remove();
function transXY(d) {
var xTrans = 0
var yTrans = 0;
if (d.tweetType == "c") {
if (d.tweetDate.getHours() <= 2) {
// could improve this to test if the open period went over midnight
// if just after midnight, then move the bar to the end of the previous day…
// have to wrap for week as well (not happening at the moment - but no early morning closes on a Sunday)
xTrans = - (x(3) - x(2));
yTrans = y(new Date(2011,0,1, 24, 0))
- barHeight(d)
- y(new Date(2011, 0, 1, d.tweetDate.getHours(), d.tweetDate.getMinutes()));
} else {
// transform bar back by its height
yTrans = - (barHeight(d));
}
}
return ("translate(" + xTrans + "," + yTrans + ")");
}
function barHeight(d) {
// positive height if tweetType is open, negative if close
// need to map the tweet height onto the timescale
var height = 0;
var rawheight = (d.tweetType )
d.tweetType
d.openEstimate
d.actualOpen
switch (d.tweetType)
{
case "c":
// d.actualOpen is expressed in hours or minutes or seconds
// can't have negative heights for a svg:rect, so to do bars for actual open need
// to add a transform to move the bar back by it's height
if (d.actualOpen.indexOf("minutes") != -1) {
var timeBits = d.actualOpen.split(" ");
// timeBits[0] will contain open time in minutes
height = y(new Date(2011, 0, 1, Math.floor(timeBits[0]/60), (timeBits[0]%60)));
} else if (d.actualOpen.indexOf("hours")) {
var timeBits = d.actualOpen.split(" ");
if (timeBits.length == 3) {
// we have format like "4 1/2 hours"
// I'm assuming it is always rounded to a 1/2 hour
height = y(new Date(2011,0,1, timeBits[0], 30));
} else {
// we have format like "three hours" or "7 hours"
var hourConverted = jQuery.inArray(timeBits[0], hourNames);
if (hourConverted != -1) {
// it was a text hour name
timeBits[0] = hourConverted + 1;
}
height = y(new Date(2011,0,1, timeBits[0],0));
}
} else if (d.actualOpen.indexOf("seconds")) {
// i think I'll ignore these!
height = 0;
} else {
// unexpected time format
height = 0;
}
// seems like some of the actual (tweetType = "c") open periods are too long
// NEED TO LOOK AT DATA & SEE WHAT IS GOING ON
// truncate at start of day, but only if after 2 am
if (d.tweetDate.getHours() >= 2) {
var maxHeight = y(new Date(2011, 0, 1, d.tweetDate.getHours(), d.tweetDate.getMinutes())) - y(new Date(2011,0,1,0,0));
if ( height > maxHeight) {
height = maxHeight;
}
}
break;
case "o":
// d.openEstimate is expressed as a time of day
var timeBits = d.openEstimate.split(":");
if (timeBits[0] == 0) {
// over boundary of 12 midnight! so just make it go to bottom of screen
// should really create a second bar the following morning going to the predicted close time
timeBits = ["24", "00"];
}
height = y(new Date(2011, 0, 1, timeBits[0], timeBits[1])) - y(new Date(2011, 0, 1, d.tweetDate.getHours(), d.tweetDate.getMinutes()));
break;
case "e":
// not processing these
height = 0;
}
return height;
}
// Button to trigger transition between circles and symbols
d3.select("#daybreakdown button#symbolToggle").on("click", function() {
daychart.selectAll("circle")
.transition()
.duration(750)
.style("opacity", (daychart.selectAll("circle").style("opacity") == 0.5) ? 1e-6 : 0.5 );
// would like to find a way to avoid selecting the same object a second time in the if test
// .style("opacity", function (d) { return (d.style("opacity") == 0.5) ? 1e-6 : 0.5 });
daychart.selectAll("path")
.transition()
.duration(750)
.style("opacity", (daychart.selectAll("path").style("opacity") == 0.5) ? 1e-6 : 0.5 );
d3.select("#daybreakdown button#symbolToggle")
.text((d3.select("#daybreakdown button#symbolToggle").text() == "Show symbols") ? "Show circles" : "Show symbols");
});
// Button to show / hide data
d3.select("#posts button").on("click", function() {
d3.select("#posts table")
.style("visibility", (d3.select("#posts table").style("visibility") == "hidden") ? "visible" : "hidden");
d3.select("#posts button")
.text((d3.select("#posts table").style("visibility") == "visible") ? "Hide data table" : "Show data table");
});
// Button to show / hide open estimate bars
d3.select("#daybreakdown button#estimatesToggle").on("click", function() {
daychart.selectAll("rect.rect_o")
.style("visibility", (d3.selectAll("rect.rect_o").style("visibility") == "hidden") ? "visible" : "hidden");
d3.select("#daybreakdown button#estimatesToggle")
.text((d3.select("#daybreakdown button#estimatesToggle").text() == "Show open estimates") ? "Hide open estimates" : "Show open estimates");
});
// Button to show / hide actual open bars
d3.select("#daybreakdown button#actualsToggle").on("click", function() {
daychart.selectAll("rect.rect_c")
.style("visibility", (d3.selectAll("rect.rect_c").style("visibility") == "hidden") ? "visible" : "hidden");
d3.select("#daybreakdown button#actualsToggle")
.text((d3.select("#daybreakdown button#actualsToggle").text() == "Show actual opens") ? "Hide actual opens" : "Show actual opens");
});
}