PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/MS Silk/Scripts/Debug/mstats.charts.js

#
JavaScript | 576 lines | 408 code | 90 blank | 78 comment | 43 complexity | 468c28a805427bde1f76b4bdd7586995 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0
  1. //===================================================================================
  2. // Microsoft patterns & practices
  3. // Silk : Web Client Guidance
  4. //===================================================================================
  5. // Copyright (c) Microsoft Corporation. All rights reserved.
  6. // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
  7. // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
  8. // LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  9. // FITNESS FOR A PARTICULAR PURPOSE.
  10. //===================================================================================
  11. // The example companies, organizations, products, domain names,
  12. // e-mail addresses, logos, people, places, and events depicted
  13. // herein are fictitious. No association with any real company,
  14. // organization, product, domain name, email address, logo, person,
  15. // places, or events is intended or should be inferred.
  16. //===================================================================================
  17. /*jslint onevar: true, undef: true, newcap: true, regexp: true, plusplus: true, bitwise: true, devel: true, maxerr: 50 */
  18. /*global jQuery:false */
  19. /* Note: jsLint and Visual Studio disagree on switch and case statement indenting.
  20. * We are using the Visual Studio indenting, so there are spacing errors when jsLint is run.
  21. */
  22. // Charts Widget
  23. // Provides Average Fuel Efficiency, Total Distance, and Total Cost charts
  24. // Provides vehicle selection (series show/hide)
  25. // Provides date range restriction (all series)
  26. // Requires: jQueryUI (slider) and jqPlot
  27. (function (mstats, $) {
  28. var animationDuration = 800,
  29. delayLength = 450;
  30. if ($.jqplot && $.jqplot.config) {
  31. $.jqplot.config.enablePlugins = true;
  32. }
  33. $.widget('mstats.charts', {
  34. // default options
  35. options: {
  36. // Default to $.ajax when sendRequest is undefined.
  37. // The extra function indirection allows $.ajax substitution because
  38. // the widget framework is using the real $.ajax during options initialization.
  39. sendRequest: function (ajaxOptions) { $.ajax(ajaxOptions); },
  40. // The name of the current chart.
  41. // 0 = Average Fuel Efficiency
  42. // 1 = Total Distance
  43. // 2 = Total Cost
  44. currentChart: 0,
  45. visible: false,
  46. invalidateData: function () { mstats.log('The invalidateData option on charts has not been set'); }
  47. },
  48. // The client-side data model for the chart (independent of jqPlot)
  49. chartData: [],
  50. getChartData: function () {
  51. return this.chartData;
  52. },
  53. // Creates the widget, taking over the UI contained in it, the links in it,
  54. // and adding the necessary behaviors.
  55. _create: function () {
  56. this.wellKnownChartTitles = ['Average Fuel Efficiency', 'Total Distance', 'Total Cost'];
  57. // There are a maximum of 10 vehicles, so I provide 10 colors.
  58. this.wellKnownSeriesColors = ["#4bb2c5", "#c5b47f", "#EAA228", "#579575", "#839557", "#958c12", "#953579", "#4b5de4", "#d8b83f", "#ff5800"];
  59. // This holds the min, max and range for the date range slider
  60. this.dateRange = {
  61. min: 0,
  62. lower: 0,
  63. upper: 0,
  64. max: 0
  65. };
  66. // This holds the date labels for each point in the date range.
  67. this.datesInDateRange = [];
  68. this._bindNavigation();
  69. this._createDateRangeSlider();
  70. this._fetchChartData();
  71. },
  72. // Initializes the jQueryUI date range slider
  73. _createDateRangeSlider: function () {
  74. var that = this;
  75. this.element.find('#slider').slider({
  76. range: true,
  77. min: 0,
  78. max: 0,
  79. values: [0, 0],
  80. slide: function (event, ui) {
  81. that.dateRange.lower = ui.values[0];
  82. that.dateRange.upper = ui.values[1];
  83. that._updateRangeDatesText();
  84. that._refreshChart();
  85. }
  86. });
  87. },
  88. // Resets the date range slider to match the dateRange min/max
  89. _resetDateRangeSlider: function () {
  90. this.element.find('#slider').slider({
  91. min: this.dateRange.min,
  92. max: this.dateRange.max - 1,
  93. values: [this.dateRange.min, this.dateRange.max - 1]
  94. });
  95. this._updateRangeDatesText();
  96. },
  97. // Updates the lower and upper date labels based on the dateRange upper/lower
  98. _updateRangeDatesText: function () {
  99. if (this.datesInDateRange.length > this.dateRange.lower) {
  100. this.element.find('#lower').html(this.datesInDateRange[this.dateRange.lower]);
  101. }
  102. if (this.datesInDateRange.length > this.dateRange.upper) {
  103. this.element.find('#upper').html(this.datesInDateRange[this.dateRange.upper]);
  104. }
  105. },
  106. // diplays the average fuel efficiency chart
  107. showMainFuelEfficiencyChart: function () {
  108. this._showChartById(0);
  109. },
  110. // diplays the total distance chart
  111. showMainDistanceChart: function () {
  112. this._showChartById(1);
  113. },
  114. // diplays the total cost chart
  115. showMainCostChart: function () {
  116. this._showChartById(2);
  117. },
  118. _showChartById: function (chartId) {
  119. if(this.options.currentChart === chartId) { return; }
  120. this.options.currentChart = chartId;
  121. this._refreshChart();
  122. },
  123. // Binds each of the chart links to showing the appropriate chart.
  124. _bindNavigation: function () {
  125. var that = this,
  126. evntName = 'click.' + this.name; // widget name => charts
  127. this.element.find('#fuel-efficiency-link').bind(evntName, function (event) {
  128. that.showMainFuelEfficiencyChart();
  129. event.preventDefault();
  130. });
  131. this.element.find('#distance-link').bind(evntName, function (event) {
  132. that.showMainDistanceChart();
  133. event.preventDefault();
  134. });
  135. this.element.find('#cost-link').bind(evntName, function (event) {
  136. that.showMainCostChart();
  137. event.preventDefault();
  138. });
  139. },
  140. refreshData: function () {
  141. this._fetchChartData();
  142. },
  143. requeryData: function () {
  144. this.options.invalidateData(this.element.data('chart-url'));
  145. this.refreshData();
  146. },
  147. // Retrieve the chart data and updates the slider.
  148. _fetchChartData: function () {
  149. var that = this,
  150. chartsTarget = this.element;
  151. if (chartsTarget) {
  152. that.element.find('#loading-message').show();
  153. that.options.sendRequest({
  154. url: chartsTarget.data('chart-url'),
  155. success: function (data) {
  156. that._updateChartData(data);
  157. that._resetDateRangeSlider();
  158. that._refreshChart();
  159. }
  160. });
  161. }
  162. },
  163. // Updates the chart data to normalize it into a client-side data model.
  164. _updateChartData: function (data) {
  165. if (!data || !data.Entries) {
  166. return;
  167. }
  168. var i,
  169. entry,
  170. month,
  171. yearAndMonth,
  172. lastSeriesId = -1,
  173. vehicleIndex = -1,
  174. dataPointIndex = 0,
  175. foundVehicles = [],
  176. foundMonths = [],
  177. fuelEfficiencyData = [],
  178. distanceData = [],
  179. costData = [];
  180. // Chart data contains 3 sets of vehicle data (fuel efficiency, distance, and cost)
  181. // Each set contains a collection of vehicle data.
  182. // Each vehicle data is a collection of X,Y data points.
  183. for (i = 0; i < data.Entries.length; i += 1) {
  184. entry = data.Entries[i];
  185. // Each time we encounter a new ID, we start a new data set for the vehicle.
  186. if (lastSeriesId !== entry.Id) {
  187. vehicleIndex += 1;
  188. dataPointIndex = 0;
  189. foundMonths[vehicleIndex] = [];
  190. foundVehicles[vehicleIndex] = { id: entry.Id, name: entry.Name };
  191. fuelEfficiencyData[vehicleIndex] = { id: entry.Id, name: entry.Name, series: [] };
  192. distanceData[vehicleIndex] = { id: entry.Id, name: entry.Name, series: [] };
  193. costData[vehicleIndex] = { id: entry.Id, name: entry.Name, series: [] };
  194. }
  195. // Year and Month is the X axis value for each data point.
  196. // We format to something that the chart can handle for a date-based axis.
  197. month = entry.Month.toString();
  198. if (entry.Month < 10) {
  199. month = '0' + month;
  200. }
  201. yearAndMonth = entry.Year.toString() + '-' + month + '-01';
  202. // We set each data point per vehicle.
  203. fuelEfficiencyData[vehicleIndex].series[dataPointIndex] = [yearAndMonth, entry.AverageFuelEfficiency];
  204. distanceData[vehicleIndex].series[dataPointIndex] = [yearAndMonth, entry.TotalDistance];
  205. costData[vehicleIndex].series[dataPointIndex] = [yearAndMonth, entry.TotalCost];
  206. // We remember each yyyy-mm-dd string
  207. foundMonths[vehicleIndex][dataPointIndex] = yearAndMonth;
  208. dataPointIndex += 1;
  209. lastSeriesId = entry.Id;
  210. }
  211. //We set the values needed for the slider
  212. this._setupDateRange(foundMonths);
  213. // We set the available vehicles for selection.
  214. this.selectableVehicles = foundVehicles;
  215. this.selectableVehiclesDirty = true;
  216. // We set the chart data.
  217. this.chartData = [fuelEfficiencyData, distanceData, costData];
  218. },
  219. _setupDateRange: function (foundMonths) {
  220. var i,
  221. lastIndex,
  222. earlyCandidate,
  223. lateCandidate,
  224. earliest,
  225. latest,
  226. allMonths = [],
  227. numFoundMonths = foundMonths.length;
  228. for (i = 0; i < numFoundMonths; i += 1) {
  229. earlyCandidate = this._convertToUTCDate(foundMonths[i][0]);
  230. if (!earliest || earlyCandidate < earliest) {
  231. earliest = earlyCandidate;
  232. }
  233. lastIndex = foundMonths[i].length - 1;
  234. lateCandidate = this._convertToUTCDate(foundMonths[i][lastIndex]);
  235. if (!latest || lateCandidate > latest) {
  236. latest = lateCandidate;
  237. }
  238. }
  239. allMonths = this._buildMonthListWithoutGaps(earliest, latest);
  240. // We set the date range based on the size of the found months
  241. this.dateRange.max = allMonths.length;
  242. this.dateRange.upper = allMonths.length - 1;
  243. this.datesInDateRange = allMonths;
  244. },
  245. _buildMonthListWithoutGaps: function (earliest, latest) {
  246. var next,
  247. start,
  248. end,
  249. pad,
  250. list = [];
  251. if (!earliest || !latest) {
  252. return [];
  253. }
  254. start = { year: earliest.getUTCFullYear(), month: earliest.getUTCMonth() };
  255. end = { year: latest.getUTCFullYear(), month: latest.getUTCMonth() };
  256. next = start;
  257. while (next.year < end.year || (next.year === end.year && next.month <= end.month)) {
  258. pad = (next.month < 10) ? '0' : '';
  259. list.push(next.year + '-' + pad + next.month + '-01');
  260. // we move the date forward to the next month
  261. if (next.month === 12) {
  262. next.year += 1;
  263. next.month = 0;
  264. }
  265. next.month += 1;
  266. }
  267. return list;
  268. },
  269. _refreshChart: function () {
  270. var data = this.chartData,
  271. current = this.options.currentChart;
  272. if(!this.options.visible) { return; }
  273. this._updateSelectableVehicleList();
  274. // If a chart is selected and there is date-based data
  275. if (current >= 0 && current < this.wellKnownChartTitles.length && this.dateRange.max > 0) {
  276. this.element.find('#main-chart-plot').show();
  277. // The currentChart indexes into the chart title and the appropraite series.
  278. if (data && (data.length > current)) {
  279. this._plotChart('#main-chart-plot', this.wellKnownChartTitles[current], data[current]);
  280. } else {
  281. this._showNoChartDataAvailableError();
  282. }
  283. } else {
  284. this._showNoChartDataAvailableError();
  285. }
  286. },
  287. // plots a chart given a target ID, title and a series of client-side data
  288. _plotChart: function (targetID, chartTitle, data) {
  289. if (!data || !data.length) {
  290. this._showNoChartDataAvailableError();
  291. return;
  292. }
  293. // We convert the client-side data to a jqPlot data format
  294. var seriesData = this._getJQPlotSeries(data),
  295. chartContainerId = 'mstats-main-chart-container',
  296. target = this.element.find(targetID);
  297. // Let's create a child container for the chart
  298. // so that we can cleanly remove it when we update it later
  299. target.children().remove();
  300. $('<div></div>').attr('id', chartContainerId).appendTo(target);
  301. if (!seriesData || !seriesData.values.length) {
  302. return;
  303. }
  304. try {
  305. $.jqplot(chartContainerId, seriesData.values, {
  306. title: chartTitle,
  307. noDataIndicator: {
  308. show: true,
  309. indicator: 'No Data to Display'
  310. },
  311. axes: {
  312. xaxis: {
  313. renderer: $.jqplot.DateAxisRenderer,
  314. tickInterval: '2 months',
  315. rendererOptions: {
  316. tickRenderer: $.jqplot.CanvasAxisTickRenderer
  317. },
  318. tickOptions: {
  319. angle: 0,
  320. fontStretch: 1,
  321. fontSize: '10pt',
  322. fontWeight: 'normal',
  323. fontFamily: 'Tahoma',
  324. formatString: '%m-%y'
  325. }
  326. }
  327. },
  328. axesDefaults: { useSeriesColor: true },
  329. cursor: {
  330. show: false,
  331. showVerticalLine: false,
  332. showHorizontalLine: false,
  333. showCursorLegend: false,
  334. showTooltip: false,
  335. zoom: false,
  336. intersectionThreshold: 6
  337. },
  338. legend: { location: 's', show: true },
  339. seriesDefaults: { showMarker: true },
  340. series: seriesData.labels.concat([{ lineWidth: 4, markerOptions: { style: 'filledCircle' } }]),
  341. seriesColors: seriesData.colors
  342. });
  343. }
  344. catch(error) {
  345. mstats.log("An error occured in jqPlot while attempting to render the chart: " + error);
  346. }
  347. this._hideNoChartDataAvailableError();
  348. },
  349. _updateSelectableVehicleList: function () {
  350. var that = this,
  351. el,
  352. checkboxes,
  353. list,
  354. i;
  355. if(!this.selectableVehiclesDirty) {
  356. return;
  357. }
  358. if (this.selectableVehicles) {
  359. list = $('<div/>');
  360. list.children().remove();
  361. for (i = 0; i < this.selectableVehicles.length; i += 1) {
  362. el = $('<div/>');
  363. el.addClass('chart-selectable-vehicle');
  364. el.append($('<div/>')
  365. .css('background-color', this.wellKnownSeriesColors[i].toString())
  366. .addClass('chart-selectable-vehicle-color'));
  367. // We have to string-concat the intial input text to make
  368. // checkboxes display property in FireFox and Chrome.
  369. checkboxes = $('<input type="checkbox"></input>')
  370. .addClass('chart-selectable-vehicle-checkbox')
  371. .attr('name', this.selectableVehicles[i].id.toString())
  372. .val(this.selectableVehicles[i].id.toString());
  373. el.append(checkboxes).append(this.selectableVehicles[i].name);
  374. el.appendTo(list);
  375. }
  376. // We must check the checkboxes after adding to the DOM.
  377. this.element.find('#vehicle-selection-list')
  378. .children()
  379. .remove()
  380. .end()
  381. .append(list.html())
  382. .find('input')
  383. .attr('checked', 'checked')
  384. .bind('click.' + this.name, function () {
  385. that._refreshChart();
  386. });
  387. }
  388. this.selectableVehiclesDirty = false;
  389. },
  390. _showNoChartDataAvailableError: function () {
  391. this.element.find('#main-chart-plot').hide().children().remove();
  392. this.element.find('#date-range-selection').hide();
  393. this.element.find('#unavailable-message').show();
  394. this.element.find('#loading-message').hide();
  395. },
  396. _hideNoChartDataAvailableError: function () {
  397. this.element.find('#loading-message').hide();
  398. this.element.find('#unavailable-message').hide();
  399. this.element.find('#date-range-selection').show();
  400. },
  401. // Converts to jqPlot data format, and provides vehicle selection and date-range for the data.
  402. _getJQPlotSeries: function (seriesData) {
  403. var vehicleSelectionList = this.element.find('#vehicle-selection'),
  404. seriesLabels = [],
  405. seriesColors = [],
  406. seriesValues = [],
  407. index = 0,
  408. data,
  409. i;
  410. // In order to pick the right series color when some vehicles are not selected, we track
  411. // the index of those added vs. the index of the item in the seris.
  412. for (i = 0; i < seriesData.length; i += 1) {
  413. data = seriesData[i];
  414. // We only consider series whose ID matches that of a checked box in the vehicle selection list.
  415. if ((data.series.length !== 0) &&
  416. (vehicleSelectionList.find('input:checked[name="' + data.id + '"]').length > 0)) {
  417. seriesLabels[index] = { label: data.name };
  418. seriesColors[index] = this.wellKnownSeriesColors[i];
  419. seriesValues[index] = this._filterSeriesByDateRange(data.series);
  420. index += 1;
  421. }
  422. }
  423. return { labels: seriesLabels, colors: seriesColors, values: seriesValues };
  424. },
  425. _filterSeriesByDateRange: function (series) {
  426. var start = this.datesInDateRange[this.dateRange.lower],
  427. end = this.datesInDateRange[this.dateRange.upper],
  428. that = this;
  429. return $.grep(series, function (item) {
  430. return that._isDateInRange(item[0], start, end);
  431. });
  432. },
  433. _isDateInRange: function (candidate, start, end) {
  434. var candidateUTC = this._convertToUTCDate(candidate),
  435. startUTC = this._convertToUTCDate(start),
  436. endUTC = this._convertToUTCDate(end);
  437. if ((candidateUTC < startUTC) || (candidateUTC > endUTC)) {
  438. return false;
  439. }
  440. return true;
  441. },
  442. _convertToUTCDate: function (dateString) {
  443. var regex = /(\d{4})-(\d{2})-(\d{2})/g,
  444. matches,
  445. year,
  446. month;
  447. // we manually parse the date string, because the implementation of Date.parse varies between browsers
  448. matches = regex.exec(dateString);
  449. year = matches[1];
  450. month = matches[2];
  451. return new Date(year, month, 1);
  452. },
  453. moveOnScreenFromRight: function () {
  454. var that = this;
  455. if (this.options.visible) { return; }
  456. this.options.visible = true;
  457. this.element.removeClass('empty')
  458. .css({ left: 500, opacity: 0, position: 'absolute' })
  459. .show()
  460. .delay(delayLength)
  461. .animate({ left: '-=500', opacity: 1 }, {
  462. duration: animationDuration,
  463. complete: function () {
  464. that.element.css({ position: 'relative' });
  465. // we need to render the chart after the animation,
  466. // otherwise the chart will not display (and possibly throw exceptions)
  467. that._refreshChart();
  468. }
  469. });
  470. },
  471. moveOffScreenToRight: function () {
  472. var that = this;
  473. if (this.options.visible) {
  474. this.element.css({ position: 'absolute', top: 0 })
  475. .animate({ left: '+=500', opacity: 0 }, {
  476. duration: animationDuration,
  477. complete: function () {
  478. that.element.css({ position: 'relative' });
  479. that.element.hide();
  480. }
  481. });
  482. this.options.visible = false;
  483. }
  484. }
  485. });
  486. } (this.mstats, jQuery));