From 4ffa18bfeac6a90d39fc33cf75f901286ec21805 Mon Sep 17 00:00:00 2001 From: Juan Pablo Alperin Date: Fri, 13 Dec 2013 21:43:22 -0600 Subject: [PATCH] Reorganized JS to make it more portable - put everything in one function - some small tweaks to look - very large changes to structure --- README.md | 55 +- alm.js | 955 ++++++++++++++++------------- assets/ui-icons_469bdd_256x240.png | Bin 0 -> 4369 bytes css/almviz.css | 40 +- css/jqueryUi.css | 15 + index.html | 36 ++ 6 files changed, 635 insertions(+), 466 deletions(-) create mode 100644 assets/ui-icons_469bdd_256x240.png create mode 100644 css/jqueryUi.css diff --git a/README.md b/README.md index 56817b5..477feee 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An example can be found on the [github page](http://jalperin.github.io/almviz/) These visualizations were inspired by conversations at the Alt-Viz hackathon group hosted by PLOS in November 2012. [More info](http://article-level-metrics.plos.org/alm-workshop-2012/hackathon/#altviz) license -------- + - - almviz is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,4 +18,55 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -See . \ No newline at end of file +See . + +# How to use # +- import alm.js +- import almviz.css and jqueryUi.css (if not already using jqueryUI) +- declare options object + - almStatsJson: The JSON response from the ALM app + - additionalStatsJson (optional): an additional source (for appending to ALM app response) + - baseUrl: URL of ALM installation for pointing users back + - minItemsToShowGraph*: assoc array with the following keys, declaring conditions for when to show graph + - minEventsForYearly + - minEventsForMonthly + - minEventsForDaily + - minYearsForYearly + - minMonthsForMonthly + - minDaysForDaily + - hasIcon: array of sources that have icons on the ALM server + - showTitle: boolean to display the title of the article at the top of the visualization + - categories: array of objects with the following keys: name, display_name, tooltip_text + - i.e., [{ name: "html", display_name: "HTML Views", tooltip_text: "Total number of HTML page views for this article. "}, { name ... }] + - vizDiv (optional): a selector where to place the whole thing (defaults to #alm) +- declare AlmViz object: var almviz = new AlmViz(options); +- initialize the Viz: almviz.initViz(); + +## Example ## + options = { + baseUrl: 'http://pkp-alm.lib.sfu.ca', + minItemsToShowGraph: { + minEventsForYearly: 6, + minEventsForMonthly: 6, + minEventsForDaily: 6, + minYearsForYearly: 6, + minMonthsForMonthly: 6, + minDaysForDaily: 6 + }, + hasIcon: ['wikipedia', 'scienceseeker', 'researchblogging', 'pubmed', 'nature', 'mendeley', 'facebook', 'crossref', 'citeulike'], + showTitle: true, + categories: [{ name: "html", display_name: "HTML Views", tooltip_text: 'Total number of HTML page views for this article. These views are recorded directly within the system itself. Overall monthly view counts may also be available.' }, + { name: "pdf", display_name: "PDF Downloads", tooltip_text: 'Total number of PDF views and downloads for this article. These views are recorded directly within the system itself. Overall monthly view counts may also be available.' }, + { name: "likes", display_name: "Likes", tooltip_text: 'Likes found in social networks such as Facebook.' }, + { name: "shares", display_name: "Shares", tooltip_text: 'Shares or bookmarks in social networks such as Facebook, CiteULike and Mendeley. In most cases, clicking on the number of shares will take you to a listing in the network itself.' }, + { name: "comments", display_name: "Comments", tooltip_text: 'Comments are .' }, + { name: "citations", display_name: "Citations", tooltip_text: 'Citations of this article found in CrossRef, PubMed and Wikipedia. In most cases, clicking on the citation count will take you to a listing in the referencing service itself.' }], + } + + d3.json('alm.json', function(data) { + options.almStatsJson = data + + var almviz = new AlmViz(options); + almviz.initViz(); + }); + diff --git a/alm.js b/alm.js index e009dfb..b5ce3a5 100644 --- a/alm.js +++ b/alm.js @@ -1,488 +1,561 @@ -// var doi = d3.select("dd#doi").attr('data-doi'); - -var baseUrl = 'http://alm.plos.org'; -// var baseUrl = ''; - -var doi = 'doi/10.1371/journal.pone.0035869'; - -var dataUrl = 'alm.json' -// var dataUrl = "/api/v3/articles/info:doi/" + doi + "?info=history"; - -var hasSVG = document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"); - -// -// Configuration for when to show graphs -// -var minEventsForYearly, minEventsForMonthly, minEventsForDaily; -var minYearsForYearly, minMonthsForMonthly, minDaysForDaily; - -minEventsForYearly = minEventsForMonthly = minEventsForDaily = 6; -minYearsForYearly = minMonthsForMonthly = minDaysForDaily = 6; - -var hasIcon = Array('wikipedia', 'scienceseeker', 'researchblogging', 'pubmed', 'nature', 'mendeley', 'facebook', 'crossref', 'citeulike'); - /** - * Extract the date from the source - * @param level (day|month|year) - * @param d the datum - * @return {Date} + * ALMViz + * See https://github.com/jalperin/almviz for more details + * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING. + * + * @brief Article level metrics visualization controller. */ -function get_date(level, d) { - switch (level) { - case 'year': - return new Date(d.year, 0, 1); - case 'month': - // js Date indexes months at 0 - return new Date(d.year, d.month - 1, 1); - case 'day': - // js Date indexes months at 0 - return new Date(d.year, d.month - 1, d.day); +function AlmViz(options) { + // allow jQuery object to be passed in + // in case a different version of jQuery is needed from the one globally defined + $ = options.jQuery || $; + + // Init data + var categories_ = options.categories; + var data = options.almStatsJson; + var additionalStats = options.additionalStatsJson; + if (additionalStats) { + data[0].sources.push(additionalStats); } -} -/** - * Format the date for display - * @param level (day|month|year) - * @param d the datum - * @return {String} - */ -function get_formatted_date(level, d) { - switch (level) { - case 'year': - return d3.time.format("%Y")(get_date(level, d)); - case 'month': - return d3.time.format("%b %y")(get_date(level, d)); - case 'day': - return d3.time.format("%d %b %y")(get_date(level, d)); - } -} + // Init basic options + var baseUrl_ = options.baseUrl; + var hasIcon = options.hasIcon; + var minItems_ = options.minItemsToShowGraph; + var showTitle = options.showTitle; + var formatNumber_ = d3.format(",d"); -/** - * - * @param level (day|month|year) - * @param source (from Json response) - * @return Array of metrics - */ -function get_data(level, source) { - switch (level) { - case 'year': - return source.by_year; - case 'month': - return source.by_month; - case 'day': - return source.by_day; - } -} + // extract publication date + var pub_date = d3.time.format.iso.parse(data[0]["publication_date"]); -/** - * Returns a d3 timeInterval for date operations - * @param level (day|month|year - * @return d3 ime Interval - */ -function get_time_interval(level) { - switch (level) { - case 'year': - return d3.time.year.utc; - case 'month': - return d3.time.month.utc; - case 'day': - return d3.time.day.utc; + var vizDiv; + // Get the Div where the viz should go (default to one with ID "alm') + if (options.vizDiv) { + vizDiv = d3.select(options.vizDiv); + } else { + vizDiv = d3.select("#alm"); } -} - -// map of category keys to labels for display -var categories = [{ name: "html", display_name: "HTML Views" }, - { name: "pdf", display_name: "PDF Downloads" }, - { name: "likes", display_name: "Likes" }, - { name: "shares", display_name: "Shares" }, - { name: "comments", display_name: "Comments" }, - { name: "citations", display_name: "Citations" }]; - -var metricsFound = false; // flag -var format_number = d3.format(",d"); // for formatting numbers for display -var charts = new Array(); // keep track of AlmViz objects - -/* Graph visualization - * The basic general set up of the graph itself - * @param chartDiv The div where the chart should go - * @param data The raw data - * @param category The category for 86 chart - */ -function AlmViz(chartDiv, pub_date, source, category) { - // size parameters - this.margin = {top: 10, right: 40, bottom: 0, left: 40}; - this.width = 400 - this.margin.left - this.margin.right; - this.height = 100 - this.margin.top - this.margin.bottom; - - // div where everything goes - this.chartDiv = chartDiv; - - // publication date - this.pub_date = pub_date; - - // source data and which category - this.category = category; - this.source = source; - - // just for record keeping - this.name = source.name + '-' + category.name; - - this.x = d3.time.scale(); - this.x.range([0, this.width]); - - this.y = d3.scale.linear(); - this.y.range([this.height, 0]); - - this.z = d3.scale.ordinal(); - this.z.range(['main', 'alt']); - // the chart - this.svg = this.chartDiv.append("svg") - .attr("width", this.width + this.margin.left + this.margin.right) - .attr("height", this.height + this.margin.top + this.margin.bottom) - .append("g") - .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")"); + // look to make sure browser support SVG + var hasSVG_ = document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"); + // to track if any metrics have been found + var metricsFound_; - // draw the bars g first so it ends up underneath the axes - this.bars = this.svg.append("g"); + /** + * Initialize the visualization. + * NB: needs to be accessible from the outside for initialization + */ + this.initViz = function() { + vizDiv.select("#loading").remove(); - // and the shadow bars on top for the tooltips - this.barsForTooltips = this.svg.append("g"); - - this.svg.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + (this.height - 1) + ")"); - this.svg.append("g") - .attr("class", "y axis"); - -} - -/** - * Takes in the basic set up of a graph and loads the data itself - * @param viz AlmViz object - * @param level string (day|month|year) - */ -function loadData(viz, level) { - d3.select("#alm > #loading").remove(); - - var pub_date = viz.pub_date - var category = viz.category; - var level_data = get_data(level, viz.source); - var timeInterval = get_time_interval(level); - - var end_date = new Date(); - // use only first 29 days if using day view - // close out the year otherwise - if ( level == 'day' ) { - end_date = timeInterval.offset(pub_date, 29); - } else { - end_date = d3.time.year.utc.ceil(end_date); - } - - // - // Domains for x and y - // - - // a time x axis, between pub_date and end_date - viz.x.domain([timeInterval.floor(pub_date), end_date]); - - // a linear axis from 0 to max value found - viz.y.domain([0, d3.max(level_data, function(d) { return d[category.name]; })]); - - // - // Axis - // - // a linear axis between publication date and current date - viz.xAxis = d3.svg.axis() - .scale(viz.x) - .tickSize(0) - .ticks(0); - - // a linear y axis between 0 and max value found in data - viz.yAxis = d3.svg.axis() - .scale(viz.y) - .orient("left") - .tickSize(0) - .tickValues([d3.max(viz.y.domain())]) // only one tick at max - .tickFormat(d3.format(",d")); - - // - // The chart itself - // - // TODO: these transitions could use a little work - var barWidth = Math.max((viz.width/(timeInterval.range(pub_date, end_date).length + 1)) - 2, 1); - - var barsForTooltips = viz.barsForTooltips.selectAll(".barsForTooltip") - .data(level_data, function(d) { return get_date(level, d); }); - - barsForTooltips - .exit() - .remove(); - - var bars = viz.bars.selectAll(".bar") - .data(level_data, function(d) { return get_date(level, d); }); - - bars - .enter().append("rect") - .attr("class", function(d) { return "bar " + viz.z((level == 'day' ? d3.time.weekOfYear(get_date(level, d)) : d.year)); }) - .attr("y", viz.height) - .attr("height", 0); - - bars - .attr("x", function(d) { return viz.x(get_date(level, d)) + 2; }) // padding of 2, 1 each side - .attr("width", barWidth); - - bars.transition() - .duration(1000) - .attr("width", barWidth) - .attr("y", function(d) { return viz.y(d[category.name]); }) - .attr("height", function(d) { return viz.height - viz.y(d[category.name]); }); - - bars - .exit().transition() - .attr("y", viz.height) - .attr("height", 0); - - bars - .exit() // .transition().delay(1000) - .remove(); - - viz.svg - .select(".x.axis") - .call(viz.xAxis); - - viz.svg - .transition().duration(1000) - .select(".y.axis") - .call(viz.yAxis); - - barsForTooltips - .enter().append("rect") - .attr("class", function(d) { return "barsForTooltip " + viz.z((level == 'day' ? d3.time.weekOfYear(get_date(level, d)) : d.year)); }); - - barsForTooltips - .attr("width", barWidth + 2) - .attr("x", function(d) { return viz.x(get_date(level, d)) + 1; }) - .attr("y", function(d) { return viz.y(d[category.name]) - 1; }) - .attr("height", function(d) { return viz.height - viz.y(d[category.name]) + 1; }); - - - // add in some tool tips - viz.barsForTooltips.selectAll("rect").each( - function(d,i){ - $(this).tooltip('destroy'); // need to destroy so all bars get updated - $(this).tooltip({title: format_number(d[category.name]) + " in " + get_formatted_date(level, d), container: "body"}); + if (showTitle) { + vizDiv.append("a") + .attr('href', 'http://dx.doi.org/' + data[0].doi) + .attr("class", "title") + .text(data[0].title); } - ); -} -d3.json(dataUrl, function(data) { - // extract publication date - var pub_date = d3.time.format.iso.parse(data[0]["publication_date"]); + // loop through categories + categories_.forEach(function(category) { + addCategory_(vizDiv, category, data); + }); - var canvas = d3.select("#alm"); - canvas.append("a") - .attr('href', 'http://dx.doi.org/' + data[0].doi) - .attr("class", "title") - .text(data[0].title); + if (!metricsFound_) { + vizDiv.append("p") + .attr("class", "muted") + .text("No metrics found."); + } + }; - // loop through categories - categories.forEach(function(category) { - canvas.append("div") - .attr("class", "alm"); - var categoryRow = false; + /** + * Build each article level statistics category. + * @param {Object} canvas d3 element + * @param {Array} category Information about the category. + * @param {Object} data Statistics. + * @return {JQueryObject|boolean} + */ + var addCategory_ = function(canvas, category, data) { + var $categoryRow = false; - // loop through sources + // Loop through sources to add statistics data to the category. data[0]["sources"].forEach(function(source) { var total = source.metrics[category.name]; - if (total > 0) { - if (!categoryRow) { - categoryRow = canvas.append("div") - .attr("class", "alm-category-row") - .attr("style", "width: 100%; overflow: hidden;") - .attr("id", "category-" + category.name); - - categoryRow.append("h2", "div.alm-category-row-heading" + category.name) - .attr("class", "border-bottom") - .attr("id", "month-" + category.name) - .text(category.display_name); - - // flag that there is at least one metric - metricsFound = true; + // Only add the category row the first time + if (!$categoryRow) { + $categoryRow = getCategoryRow_(canvas, category); } - var row = categoryRow - .append("div") - .attr("class", "alm-row") - .attr("style", "float: left") - .attr("id", "alm-row-" + source.name + "-" + category.name); - - var countLabel = row.append("div") - .attr("class", "alm-count-label"); - - if (hasIcon.indexOf(source.name) >= 0) { - countLabel.append("img") - .attr("src", baseUrl + '/assets/' + source.name + '.png') - .attr("alt", 'a description of the source') - .attr("class", "label-img"); - } + // Flag that there is at least one metric + metricsFound_ = true; + addSource_(source, total, category, $categoryRow); + } + }); + }; + + + /** + * Get category row d3 HTML element. It will automatically + * add the element to the passed canvas. + * @param {d3Object} canvas d3 HTML element + * @param {Array} category Category information. + * @param {d3Object} + */ + var getCategoryRow_ = function(canvas, category) { + var categoryRow, categoryTitle, tooltip; + + // Build category html objects. + categoryRow = canvas.append("div") + .attr("class", "alm-category-row") + .attr("style", "width: 100%; overflow: hidden;") + .attr("id", "category-" + category.name); + + categoryTitle = categoryRow.append("h2") + .attr("class", "alm-category-row-heading") + .attr("id", "month-" + category.name) + .text(category.display_name); + + tooltip = categoryTitle.append("div") + .attr("class", "alm-category-row-info").append("span") + .attr("class", "ui-icon ui-icon-info"); + + $(tooltip).tooltip({title: category.tooltip_text, container: 'body'}); + + return categoryRow; + }; + + + /** + * Add source information to the passed category row element. + * @param {Object} source + * @param {integer} sourceTotalValue + * @param {Object} category + * @param {JQueryObject} $categoryRow + * @return {JQueryObject} + */ + var addSource_ = function(source, sourceTotalValue, category, $categoryRow) { + var $row, $countLabel, $count, + total = sourceTotalValue; + + $row = $categoryRow + .append("div") + .attr("class", "alm-row") + .attr("style", "float: left") + .attr("id", "alm-row-" + source.name + "-" + category.name); + + $countLabel = $row.append("div") + .attr("class", "alm-count-label"); + + if (hasIcon.indexOf(source.name) >= 0) { + $countLabel.append("img") + .attr("src", baseUrl_ + '/assets/' + source.name + '.png') + .attr("alt", 'a description of the source') + .attr("class", "label-img"); + } - var count; - if (source.events_url) { - // if there is an events_url, we can link to it from the count - count = countLabel.append("a") - .attr("href", function(d) { return source.events_url; }); - } else { - // if no events_url, we just put in the count - count = countLabel.append("span"); - } + if (source.events_url) { + // if there is an events_url, we can link to it from the count + $count = $countLabel.append("a") + .attr("href", function(d) { return source.events_url; }); + } else { + // if no events_url, we just put in the count + $count = $countLabel.append("span"); + } - count - .attr("class", "alm-count") - .attr("id", "alm-count-" + source.name + "-" + category.name) - .text(function(d) { return format_number(total); }); + $count + .attr("class", "alm-count") + .attr("id", "alm-count-" + source.name + "-" + category.name) + .text(formatNumber_(total)); + + $countLabel.append("br"); + + if (source.name == 'pkpTimedViews') { + $countLabel.append("span") + .text(source.display_name); + } else { + // link the source name + $countLabel.append("a") + .attr("href", baseUrl_ + "/sources/" + source.name) + .text(source.display_name); + } + // Only add a chart if the browser supports SVG + if (hasSVG_) { + var level = false; + + // check what levels we can show + var showDaily = false; + var showMonthly = false; + var showYearly = false; + + if (source.by_year) { + level_data = getData_('year', source); + var yearTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); + var numYears = d3.time.year.utc.range(pub_date, new Date()).length; + + if (yearTotal >= minItems_.minEventsForYearly && + numYears >= minItems_.minYearsForYearly) { + showYearly = true; + level = 'year'; + }; + } - countLabel.append("br"); + if (source.by_month) { + level_data = getData_('month', source); + var monthTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); + var numMonths = d3.time.month.utc.range(pub_date, new Date()).length; - // link the source name - countLabel.append("a") - .attr("href", function(d) { return baseUrl + "/sources/" + source.name; }) - .text(function(d) { return source.display_name; }); + if (monthTotal >= minItems_.minEventsForMonthly && + numMonths >= minItems_.minMonthsForMonthly) { + showMonthly = true; + level = 'month'; + }; } - // If there is not SVG, do not even try the charts - if ( hasSVG ) { - var level = false; + if (source.by_day){ + level_data = getData_('day', source); + var dayTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); + var numDays = d3.time.day.utc.range(pub_date, new Date()).length; - // check what levels we can show - var showDaily = false; - var showMonthly = false; - var showYearly = false; + if (dayTotal >= minItems_.minEventsForDaily && numDays >= minItems_.minDaysForDaily) { + showDaily = true; + level = 'day'; + }; + } - if (source.by_year) { - level_data = get_data('year', source); - var yearTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); - var numYears = d3.time.year.utc.range(pub_date, new Date()).length + // The level and level_data should be set to the finest level + // of granularity that we can show + timeInterval = getTimeInterval_(level); - if (yearTotal >= minEventsForYearly && numYears >= minYearsForYearly) { - showYearly = true; - level = 'year'; - } - } + // check there is data for + if (showDaily || showMonthly || showYearly) { + var $chartDiv = $row.append("div") + .attr("style", "width: 70%; float:left;") + .attr("class", "alm-chart-area"); - if (source.by_month) { - level_data = get_data('month', source); - var monthTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); - var numMonths = d3.time.month.utc.range(pub_date, new Date()).length + var viz = getViz_($chartDiv, source, category); + loadData_(viz, level); - if (monthTotal >= minEventsForMonthly && numMonths >= minMonthsForMonthly) { - showMonthly = true; - level = 'month'; - } - } + var update_controls = function(control) { + control.siblings('.alm-control').removeClass('active'); + control.addClass('active'); + }; - if (source.by_day){ - level_data = get_data('day', source); - var dayTotal = level_data.reduce(function(i, d) { return i + d[category.name]; }, 0); - var numDays = d3.time.day.utc.range(pub_date, new Date()).length + var $levelControlsDiv = $chartDiv.append("div") + .attr("style", "width: " + (viz.margin.left + viz.width) + "px;") + .append("div") + .attr("style", "float:right;"); + + if (showDaily) { + $levelControlsDiv.append("a") + .attr("href", "javascript:void(0)") + .classed("alm-control", true) + .classed("disabled", !showDaily) + .classed("active", (level == 'day')) + .text("daily (first 30)") + .on("click", function() { + if (showDaily && !$(this).hasClass('active')) { + loadData_(viz, 'day'); + update_controls($(this)); + } + } + ); - if (dayTotal >= minEventsForDaily && numDays >= minDaysForDaily) { - showDaily = true; - level = 'day'; - } + $levelControlsDiv.append("text").text(" | "); } - // The level level_data should be set to the finest level - // of granularity that we can show - timeInterval = get_time_interval(level); - - // check there is data for - if ( showDaily || showMonthly || showYearly ) { - var chartDiv = row.append("div") - .attr("style", "width: 70%; float:left;") - .attr("class", "alm-chart-area"); - - var viz = new AlmViz(chartDiv, pub_date, source, category); - loadData(viz, level); - - var update_controls = function(control) { - control.siblings('.alm-control').removeClass('active'); - control.addClass('active') - } - var levelControlsDiv = chartDiv.append("div") - .attr("style", "width: " + (viz.margin.left + viz.width) + "px;") - .append("div") - .attr("style", "float:right;"); - - if (showDaily) { - levelControlsDiv.append("a") - .attr("href", "javascript:void(0)") - .classed("alm-control", true) - .classed("disabled", !showDaily) - .classed("active", (level == 'day')) - .text("daily (first 30)") - .on("click", function() { if (showDaily && !$(this).hasClass('active')) { - loadData(viz, 'day'); - update_controls($(this)); - } }); - - levelControlsDiv.append("text") - .text(" | "); - } - - if (showMonthly) { - levelControlsDiv.append("a") - .attr("href", "javascript:void(0)") - .classed("alm-control", true) - .classed("disabled", !showMonthly || !showYearly) - .classed("active", (level == 'month')) - .text("monthly") - .on("click", function() { if (showMonthly && !$(this).hasClass('active')) { - loadData(viz, 'month'); - update_controls($(this)); - } }); - - if (showYearly) { - levelControlsDiv.append("text") - .text(" | "); - } - - } + if (showMonthly) { + $levelControlsDiv.append("a") + .attr("href", "javascript:void(0)") + .classed("alm-control", true) + .classed("disabled", !showMonthly || !showYearly) + .classed("active", (level == 'month')) + .text("monthly") + .on("click", function() { if (showMonthly && !$(this).hasClass('active')) { + loadData_(viz, 'month'); + update_controls($(this)); + } }); if (showYearly) { - levelControlsDiv.append("a") - .attr("href", "javascript:void(0)") - .classed("alm-control", true) - .classed("disabled", !showYearly || !showMonthly) - .classed("active", (level == 'year')) - .text("yearly") - .on("click", function() { if (showYearly && !$(this).hasClass('active')) { - loadData(viz, 'year'); - update_controls($(this)); - } }); + $levelControlsDiv.append("text") + .text(" | "); } - // keep track of all instances (mostly for debugging at this point) - charts[source.name + '-' + category.name] = viz; - - // add a clearer and styles to ensure graphs on their own line - row.insert("div", ":first-child") - .attr('style', 'clear:both') - row.attr('style', "width: 100%") + } + if (showYearly) { + $levelControlsDiv.append("a") + .attr("href", "javascript:void(0)") + .classed("alm-control", true) + .classed("disabled", !showYearly || !showMonthly) + .classed("active", (level == 'year')) + .text("yearly") + .on("click", function() { + if (showYearly && !$(this).hasClass('active')) { + loadData_(viz, 'year'); + update_controls($(this)); + } + } + ); } - } - }); - }); - if (!metricsFound) { - canvas.append("p") - .attr("class", "muted") - .text("No metrics found."); + // add a clearer and styles to ensure graphs on their own line + $row.insert("div", ":first-child") + .attr('style', 'clear:both'); + $row.attr('style', "width: 100%"); + }; + }; + + return $row; + }; + + + /** + * Extract the date from the source + * @param level (day|month|year) + * @param d the datum + * @return {Date} + */ + var getDate_ = function(level, d) { + switch (level) { + case 'year': + return new Date(d.year, 0, 1); + case 'month': + // js Date indexes months at 0 + return new Date(d.year, d.month - 1, 1); + case 'day': + // js Date indexes months at 0 + return new Date(d.year, d.month - 1, d.day); + } + }; + + + /** + * Format the date for display + * @param level (day|month|year) + * @param d the datum + * @return {String} + */ + var getFormattedDate_ = function(level, d) { + switch (level) { + case 'year': + return d3.time.format("%Y")(getDate_(level, d)); + case 'month': + return d3.time.format("%b %y")(getDate_(level, d)); + case 'day': + return d3.time.format("%d %b %y")(getDate_(level, d)); + } + }; + + + /** + * Extract the data from the source. + * @param {string} level (day|month|year) + * @param {Object} source + * @return {Array} Metrics + */ + var getData_ = function(level, source) { + switch (level) { + case 'year': + return source.by_year; + case 'month': + return source.by_month; + case 'day': + return source.by_day; + } + }; + + /** + * Returns a d3 timeInterval for date operations. + * @param {string} level (day|month|year + * @return {Object} d3 time Interval + */ + var getTimeInterval_ = function(level) { + switch (level) { + case 'year': + return d3.time.year.utc; + case 'month': + return d3.time.month.utc; + case 'day': + return d3.time.day.utc; + } + }; + + /** + * The basic general set up of the graph itself + * @param {JQueryElement} chartDiv The div where the chart should go + * @param {Object} source + * @param {Array} category The category for 86 chart + * @return {Object} + */ + var getViz_ = function(chartDiv, source, category) { + var viz = {}; + + // size parameters + viz.margin = {top: 10, right: 40, bottom: 0, left: 40}; + viz.width = 400 - viz.margin.left - viz.margin.right; + viz.height = 100 - viz.margin.top - viz.margin.bottom; + + // div where everything goes + viz.chartDiv = chartDiv; + + // source data and which category + viz.category = category; + viz.source = source; + + // just for record keeping + viz.name = source.name + '-' + category.name; + + viz.x = d3.time.scale(); + viz.x.range([0, viz.width]); + + viz.y = d3.scale.linear(); + viz.y.range([viz.height, 0]); + + viz.z = d3.scale.ordinal(); + viz.z.range(['main', 'alt']); + + // the chart + viz.svg = viz.chartDiv.append("svg") + .attr("width", viz.width + viz.margin.left + viz.margin.right) + .attr("height", viz.height + viz.margin.top + viz.margin.bottom) + .append("g") + .attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")"); + + + // draw the bars g first so it ends up underneath the axes + viz.bars = viz.svg.append("g"); + + // and the shadow bars on top for the tooltips + viz.barsForTooltips = viz.svg.append("g"); + + viz.svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + (viz.height - 1) + ")"); + + viz.svg.append("g") + .attr("class", "y axis"); + + return viz; + }; + + + /** + * Takes in the basic set up of a graph and loads the data itself + * @param {Object} viz AlmViz object + * @param {string} level (day|month|year) + */ + var loadData_ = function(viz, level) { + var category = viz.category; + var level_data = getData_(level, viz.source); + var timeInterval = getTimeInterval_(level); + + var end_date = new Date(); + // use only first 29 days if using day view + // close out the year otherwise + if (level == 'day') { + end_date = timeInterval.offset(pub_date, 29); + } else { + end_date = d3.time.year.utc.ceil(end_date); + } + + // + // Domains for x and y + // + // a time x axis, between pub_date and end_date + viz.x.domain([timeInterval.floor(pub_date), end_date]); + + // a linear axis from 0 to max value found + viz.y.domain([0, d3.max(level_data, function(d) { return d[category.name]; })]); + + // + // Axis + // + // a linear axis between publication date and current date + viz.xAxis = d3.svg.axis() + .scale(viz.x) + .tickSize(0) + .ticks(0); + + // a linear y axis between 0 and max value found in data + viz.yAxis = d3.svg.axis() + .scale(viz.y) + .orient("left") + .tickSize(0) + .tickValues([d3.max(viz.y.domain())]) // only one tick at max + .tickFormat(d3.format(",d")); + + // + // The chart itself + // + + // TODO: these transitions could use a little work + var barWidth = Math.max((viz.width/(timeInterval.range(pub_date, end_date).length + 1)) - 2, 1); + + var barsForTooltips = viz.barsForTooltips.selectAll(".barsForTooltip") + .data(level_data, function(d) { return getDate_(level, d); }); + + barsForTooltips + .exit() + .remove(); + + var bars = viz.bars.selectAll(".bar") + .data(level_data, function(d) { return getDate_(level, d); }); + + bars + .enter().append("rect") + .attr("class", function(d) { return "bar " + viz.z((level == 'day' ? d3.time.weekOfYear(getDate_(level, d)) : d.year)); }) + .attr("y", viz.height) + .attr("height", 0); + + bars + .attr("x", function(d) { return viz.x(getDate_(level, d)) + 2; }) // padding of 2, 1 each side + .attr("width", barWidth); + + bars.transition() + .duration(1000) + .attr("width", barWidth) + .attr("y", function(d) { return viz.y(d[category.name]); }) + .attr("height", function(d) { return viz.height - viz.y(d[category.name]); }); + + bars + .exit().transition() + .attr("y", viz.height) + .attr("height", 0); + + bars + .exit() + .remove(); + + viz.svg + .select(".x.axis") + .call(viz.xAxis); + + viz.svg + .transition().duration(1000) + .select(".y.axis") + .call(viz.yAxis); + + barsForTooltips + .enter().append("rect") + .attr("class", function(d) { return "barsForTooltip " + viz.z((level == 'day' ? d3.time.weekOfYear(getDate_(level, d)) : d.year)); }); + + barsForTooltips + .attr("width", barWidth + 2) + .attr("x", function(d) { return viz.x(getDate_(level, d)) + 1; }) + .attr("y", function(d) { return viz.y(d[category.name]) - 1; }) + .attr("height", function(d) { return viz.height - viz.y(d[category.name]) + 1; }); + + + // add in some tool tips + viz.barsForTooltips.selectAll("rect").each( + function(d,i){ + $(this).tooltip('destroy'); // need to destroy so all bars get updated + $(this).tooltip({title: formatNumber_(d[category.name]) + " in " + getFormattedDate_(level, d), container: "body"}); + } + ); } -}); +} \ No newline at end of file diff --git a/assets/ui-icons_469bdd_256x240.png b/assets/ui-icons_469bdd_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2cf079add1ca236adeb509698adabbffb08acb GIT binary patch literal 4369 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 + +

ALM-VIZ

@@ -16,7 +23,36 @@

ALM-VIZ

Metrics Loading...
+ +

Built with d3.js

\ No newline at end of file