PageSourceSearch

https://vestnik-ngo.kz/plugins/metrics/alm/js/alm.js

js vestnik-ngo.kz collected 2026-10-02 16:37:24 UTC 14,641 bytes, 529 lines download raw bytes

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.