1/** 2 * ALMViz 3 * See https://github.com/jalperin/almviz for more details 4 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING. 5 * 6 * @brief Article level metrics visualization controller. 7 */ 8function AlmViz(options) { 9 // allow jQuery object to be passed in 10 // in case a different version of jQuery is needed from the one globally defined 11 var $ = options.jQuery || $; 12 13 // Init data 14 var categories_ = options.categories; 15 var data = options.almStatsJson; 16 var additionalStats = options.additionalStatsJson; 17 if (additionalStats) { 18 data[0].sources.push(additionalStats); 19 } 20 21 // Init basic options 22 var baseUrl_ = options.baseUrl; 23 var hasIcon = options.hasIcon; 24 var minItems_ = options.minItemsToShowGraph; 25 var showTitle = options.showTitle; 26 var formatNumber_ = d3.format(",d"); 27 28 // extract publication date 29 var pub_date = d3.time.format.iso.parse(data[0]["publication_date"]); 30 31 var vizDiv; 32 // Get the Div where the viz should go (default to one with ID "alm') 33 if (options.vizDiv) { 34 vizDiv = d3.select(options.vizDiv); 35 } else { 36 vizDiv = d3.select("#alm"); 37 } 38 39 // look to make sure browser support SVG 40 var hasSVG_ = document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"); 41 42 // to track if any metrics have been found 43 var metricsFound_; 44 45 /** 46 * Initialize the visualization. 47 * NB: needs to be accessible from the outside for initialization 48 */ 49 this.initViz = function() { 50 vizDiv.select("#loading").remove(); 51 52 if (showTitle) { 53 vizDiv.append("a") 54 .attr('href', 'https://doi.org/' + data[0].doi) 55 .attr("class", "title") 56 .text(data[0].title); 57 } 58 59 // loop through categories 60 categories_.forEach(function(category) { 61 addCategory_(vizDiv, category, data); 62 }); 63 64 65 if (!metricsFound_) { 66 vizDiv.append("p") 67 .attr("class", "muted") 68 .text("No metrics found."); 69 } 70 }; 71 72 73 /** 74 * Build each article level statistics category. 75 * @param {Object} canvas d3 element 76 * @param {Array} category Information about the category. 77 * @param {Object} data Statistics. 78 * @return {JQueryObject|boolean} 79 */ 80 var addCategory_ = function(canvas, category, data) { 81 var $categoryRow = false; 82 83 // Loop through sources to add statistics data to the category. 84 data[0]["sources"].forEach(function(source) { 85 var total = source.metrics[category.name]; 86 if (total > 0) { 87 // Only add the category row the first time 88 if (!$categoryRow) { 89 $categoryRow = getCategoryRow_(canvas, category, total); 90 } 91 92 // Flag that there is at least one metric 93 metricsFound_ = true; 94 addSource_(source, total, category, $categoryRow); 95 } 96 }); 97 }; 98 99 100 /** 101 * Get category row d3 HTML element. It will automatically 102 * add the element to the passed canvas. 103 * @param {d3Object} canvas d3 HTML element 104 * @param {Array} category Category information. 105 * @param {d3Object} 106 */ 107 var getCategoryRow_ = function(canvas, category, sourceTotalValue) { 108 var categoryRow, categoryTitle, tooltip; 109 110 // Build category html objects. 111 categoryRow = canvas.append("div") 112 .attr("class", "alm-category-row") 113 .attr("style", "width: 100%; overflow: hidden;") 114 .attr("id", "category-" + category.name); 115 116 categoryTitle = categoryRow.append("div") 117 .attr("class", "alm-category-row-heading") 118 .attr("id", "month-" + category.name) 119 .text(category.display_name + " " + formatNumber_(sourceTotalValue)); 120 121 tooltip = categoryTitle.append("div") 122 .attr("class", "alm-category-row-info").append("span") 123 .attr("class", "ui-icon ui-icon-info"); 124 125 $(tooltip).tooltip({title: category.tooltip_text, container: 'body'}); 126 127 return categoryRow; 128 }; 129 130 131 /** 132 * Add source information to the passed category row element. 133 * @param {Object} source 134 * @param {integer} sourceTotalValue 135 * @param {Object} category 136 * @param {JQueryObject} $categoryRow 137 * @return {JQueryObject} 138 */ 139 var addSource_ = function(source, sourceTotalValue, category, $categoryRow) { 140 var $row, $countLabel, $count, 141 total = sourceTotalValue; 142 143 $row = $categoryRow 144 .append("div") 145 .attr("class", "alm-row") 146 .attr("style", "float: left") 147 .attr("id", "alm-row-" + source.name + "-" + category.name); 148 149 // Only add a chart if the browser supports SVG and we have 150 // data by time dimension. 151 level_data = getData_('year', source); 152 if (hasSVG_ && level_data && level_data[0].hasOwnProperty(category.name)) { 153 var level = false;
154 155 // check what levels we can show 156 var showDaily = false; 157 var showMonthly = false; 158 var showYearly = false; 159 160 if (source.by_year) { 161 level_data = getData_('year', source); 162 var yearTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); 163 var numYears = d3.time.year.utc.range(pub_date, new Date()).length; 164 165 if (yearTotal >= minItems_.minEventsForYearly && 166 numYears >= minItems_.minYearsForYearly) { 167 showYearly = true; 168 level = 'year'; 169 }; 170 } 171 172 if (source.by_month) { 173 level_data = getData_('month', source); 174 var monthTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); 175 var numMonths = d3.time.month.utc.range(pub_date, new Date()).length; 176 177 if (monthTotal >= minItems_.minEventsForMonthly && 178 numMonths >= minItems_.minMonthsForMonthly) { 179 showMonthly = true; 180 level = 'month'; 181 }; 182 } 183 184 if (source.by_day){ 185 level_data = getData_('day', source); 186 var dayTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); 187 var numDays = d3.time.day.utc.range(pub_date, new Date()).length; 188 189 if (dayTotal >= minItems_.minEventsForDaily && numDays >= minItems_.minDaysForDaily) { 190 showDaily = true; 191 level = 'day'; 192 }; 193 } 194 195 // The level and level_data should be set to the finest level 196 // of granularity that we can show 197 timeInterval = getTimeInterval_(level); 198 199 // check there is data for 200 if (showDaily || showMonthly || showYearly) { 201 var $chartDiv = $row.append("div") 202 .attr("style", "width: 70%; float:left;") 203 .attr("class", "alm-chart-area"); 204 205 var viz = getViz_($chartDiv, source, category); 206 loadData_(viz, level); 207 208 var update_controls = function(control) { 209 control.siblings('.alm-control').removeClass('active'); 210 control.addClass('active'); 211 }; 212 213 var $levelControlsDiv = $chartDiv.append("div") 214 .attr("style", "width: " + (viz.margin.left + viz.width) + "px;") 215 .append("div") 216 .attr("style", "float:right;"); 217 218 if (showDaily) { 219 $levelControlsDiv.append("a") 220 .attr("href", "javascript:void(0)") 221 .classed("alm-control", true) 222 .classed("disabled", !showDaily) 223 .classed("active", (level == 'day')) 224 .text("daily (first 30)") 225 .on("click", function() { 226 if (showDaily && !$(this).hasClass('active')) { 227 loadData_(viz, 'day'); 228 update_controls($(this)); 229 } 230 } 231 ); 232 233 $levelControlsDiv.append("text").text(" | "); 234 } 235 236 if (showMonthly) { 237 $levelControlsDiv.append("a") 238 .attr("href", "javascript:void(0)") 239 .classed("alm-control", true) 240 .classed("disabled", !showMonthly || !showYearly) 241 .classed("active", (level == 'month')) 242 .text("monthly") 243 .on("click", function() { if (showMonthly && !$(this).hasClass('active')) { 244 loadData_(viz, 'month'); 245 update_controls($(this)); 246 } }); 247 248 if (showYearly) { 249 $levelControlsDiv.append("text") 250 .text(" | "); 251 } 252 253 } 254 255 if (showYearly) { 256 $levelControlsDiv.append("a") 257 .attr("href", "javascript:void(0)") 258 .classed("alm-control", true) 259 .classed("disabled", !showYearly || !showMonthly) 260 .classed("active", (level == 'year')) 261 .text("yearly") 262 .on("click", function() { 263 if (showYearly && !$(this).hasClass('active')) { 264 loadData_(viz, 'year'); 265 update_controls($(this)); 266 } 267 } 268 ); 269 } 270 271 // add a clearer and styles to ensure graphs on their own line 272 $row.insert("div", ":first-child") 273 .attr('style', 'clear:both'); 274 $row.attr('style', "width: 100%"); 275 }; 276 }; 277 278 return $row; 279 }; 280 281 282 /** 283 * Extract the date from the source 284 * @param level (day|month|year) 285 * @param d the datum 286 * @return {Date} 287 */ 288 var getDate_ = function(level, d) { 289 switch (level) { 290 case 'year': 291 return new Date(d.year, 0, 1); 292 case 'month': 293 // js Date indexes months at 0 294 return new Date(d.year, d.month - 1, 1); 295 case 'day': 296 // js Date indexes months at 0 297 return new Date(d.year, d.month - 1, d.day); 298 } 299 }; 300 301 302 /** 303 * Format the date for display 304 * @param level (day|month|year) 305 * @param d the datum 306 * @return {String} 307 */ 308 var getFormattedDate_ = function(level, d) { 309 switch (level) { 310 case 'year': 311 return d3.time.format("%Y")(getDate_(level, d)); 312 case 'month': 313 return d3.time.format("%b %y")(getDate_(level, d)); 314 case 'day': 315 return d3.time.format("%d %b %y")(getDate_(level, d)); 316 } 317 }; 318 319 320 /** 321 * Extract the data from the source. 322 * @param {string} level (day|month|year) 323 * @param {Object} source 324 * @return {Array|null} Metrics or null 325 */ 326 var getData_ = function(level, source) { 327 switch (level) { 328 case 'year': 329 if (source.by_year) return source.by_year; 330 case 'month': 331 if (source.by_month) return source.by_month; 332 case 'day': 333 if (source.by_day) return source.by_day; 334 } 335 336 return null; 337 }; 338 339 /** 340 * Returns a d3 timeInterval for date operations. 341 * @param {string} level (day|month|year 342 * @return {Object} d3 time Interval 343 */ 344 var getTimeInterval_ = function(level) { 345 switch (level) { 346 case 'year': 347 return d3.time.year.utc; 348 case 'month': 349 return d3.time.month.utc; 350 case 'day': 351 return d3.time.day.utc; 352 } 353 }; 354 355 /** 356 * The basic general set up of the graph itself 357 * @param {JQueryElement} chartDiv The div where the chart should go 358 * @param {Object} source 359 * @param {Array} category The category for 86 chart 360 * @return {Object} 361 */
362 var getViz_ = function(chartDiv, source, category) { 363 var viz = {}; 364 365 // size parameters 366 viz.margin = {top: 10, right: 40, bottom: 0, left: 40}; 367 viz.width = 400 - viz.margin.left - viz.margin.right; 368 viz.height = 100 - viz.margin.top - viz.margin.bottom; 369 370 // div where everything goes 371 viz.chartDiv = chartDiv; 372 373 // source data and which category 374 viz.category = category; 375 viz.source = source; 376 377 // just for record keeping 378 viz.name = source.name + '-' + category.name; 379 380 viz.x = d3.time.scale(); 381 viz.x.range([0, viz.width]); 382 383 viz.y = d3.scale.linear(); 384 viz.y.range([viz.height, 0]); 385 386 viz.z = d3.scale.ordinal(); 387 viz.z.range(['main', 'alt']); 388 389 // the chart 390 viz.svg = viz.chartDiv.append("svg") 391 .attr("width", viz.width + viz.margin.left + viz.margin.right) 392 .attr("height", viz.height + viz.margin.top + viz.margin.bottom) 393 .append("g") 394 .attr("transform", "translate(" + viz.margin.left + "," + (viz.margin.top - 5) + ")"); 395 396 397 // draw the bars g first so it ends up underneath the axes 398 viz.bars = viz.svg.append("g"); 399 400 // and the shadow bars on top for the tooltips 401 viz.barsForTooltips = viz.svg.append("g"); 402 403 viz.svg.append("g") 404 .attr("class", "x axis") 405 .attr("transform", "translate(0," + (viz.height - 1) + ")"); 406 407 viz.svg.append("g") 408 .attr("class", "y axis"); 409 410 return viz; 411 }; 412 413 414 /** 415 * Takes in the basic set up of a graph and loads the data itself 416 * @param {Object} viz AlmViz object 417 * @param {string} level (day|month|year) 418 */ 419 var loadData_ = function(viz, level) { 420 var category = viz.category; 421 var level_data = getData_(level, viz.source); 422 var timeInterval = getTimeInterval_(level); 423 424 var end_date = new Date(); 425 // use only first 29 days if using day view 426 // close out the year otherwise 427 if (level == 'day') { 428 end_date = timeInterval.offset(pub_date, 29); 429 } else { 430 end_date = d3.time.year.utc.ceil(end_date); 431 } 432 433 // 434 // Domains for x and y 435 // 436 // a time x axis, between pub_date and end_date 437 viz.x.domain([timeInterval.floor(pub_date), end_date]); 438 439 // a linear axis from 0 to max value found 440 viz.y.domain([0, d3.max(level_data, function(d) { return d[category.name]; })]); 441 442 // 443 // Axis 444 // 445 // a linear axis between publication date and current date 446 viz.xAxis = d3.svg.axis() 447 .scale(viz.x) 448 .tickSize(0) 449 .ticks(0); 450 451 // a linear y axis between 0 and max value found in data 452 viz.yAxis = d3.svg.axis() 453 .scale(viz.y) 454 .orient("left") 455 .tickSize(0) 456 .tickValues([d3.max(viz.y.domain())]) // only one tick at max 457 .tickFormat(d3.format(",d")); 458 459 // 460 // The chart itself 461 // 462 463 // TODO: these transitions could use a little work 464 var barWidth = Math.max((viz.width/(timeInterval.range(pub_date, end_date).length + 1)) - 2, 1); 465 466 var barsForTooltips = viz.barsForTooltips.selectAll(".barsForTooltip") 467 .data(level_data, function(d) { return getDate_(level, d); }); 468 469 barsForTooltips 470 .exit() 471 .remove(); 472 473 var bars = viz.bars.selectAll(".bar") 474 .data(level_data, function(d) { return getDate_(level, d); }); 475 476 bars 477 .enter().append("rect") 478 .attr("class", function(d) { return "bar " + viz.z((level == 'day' ? d3.time.weekOfYear(getDate_(level, d)) : d.year)); }) 479 .attr("y", viz.height) 480 .attr("height", 0); 481 482 bars 483 .attr("x", function(d) { return viz.x(getDate_(level, d)) + 2; }) // padding of 2, 1 each side 484 .attr("width", barWidth); 485 486 bars.transition() 487 .duration(1000) 488 .attr("width", barWidth) 489 .attr("y", function(d) { return viz.y(d[category.name]); }) 490 .attr("height", function(d) { return viz.height - viz.y(d[category.name]); }); 491 492 bars 493 .exit().transition() 494 .attr("y", viz.height) 495 .attr("height", 0); 496 497 bars 498 .exit() 499 .remove(); 500 501 viz.svg 502 .select(".x.axis") 503 .call(viz.xAxis); 504 505 viz.svg 506 .transition().duration(1000) 507 .select(".y.axis") 508 .call(viz.yAxis); 509 510 barsForTooltips 511 .enter().append("rect") 512 .attr("class", function(d) { return "barsForTooltip " + viz.z((level == 'day' ? d3.time.weekOfYear(getDate_(level, d)) : d.year)); }); 513 514 barsForTooltips 515 .attr("width", barWidth + 2) 516 .attr("x", function(d) { return viz.x(getDate_(level, d)) + 1; }) 517 .attr("y", function(d) { return viz.y(d[category.name]) - 1; }) 518 .attr("height", function(d) { return viz.height - viz.y(d[category.name]) + 1; }); 519 520 521 // add in some tool tips 522 viz.barsForTooltips.selectAll("rect").each( 523 function(d,i){ 524 $(this).tooltip('destroy'); // need to destroy so all bars get updated 525 $(this).tooltip({title: formatNumber_(d[category.name]) + " in " + getFormattedDate_(level, d), container: "body"}); 526 } 527 ); 528 } 529}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.