heatmaps.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /* ------------------------------------------------------------------------------
  2. *
  3. * # D3.js - horizontal bar chart
  4. *
  5. * Demo d3.js horizontal bar chart setup with .csv data source
  6. *
  7. * ---------------------------------------------------------------------------- */
  8. // Setup module
  9. // ------------------------------
  10. var DashboardHeatmaps = function() {
  11. //
  12. // Setup module components
  13. //
  14. // Daily sales heatmap
  15. var _AppSalesHeatmap = function(element) {
  16. if (typeof d3 == 'undefined') {
  17. console.warn('Warning - d3.min.js is not loaded.');
  18. return;
  19. }
  20. // Initialize chart only if element exsists in the DOM
  21. if($(element).length > 0) {
  22. // Load data
  23. d3.csv('../../../../global_assets/demo_data/dashboard/app_sales_heatmap.csv', function(error, data) {
  24. // Bind data
  25. // ------------------------------
  26. // Nest data
  27. var nested_data = d3.nest().key(function(d) { return d.app; }),
  28. nest = nested_data.entries(data);
  29. // Format date
  30. var format = d3.time.format('%Y/%m/%d %H:%M'),
  31. formatTime = d3.time.format('%H:%M');
  32. // Pull out values
  33. data.forEach(function(d, i) {
  34. d.date = format.parse(d.date),
  35. d.value = +d.value
  36. });
  37. // Layout setup
  38. // ------------------------------
  39. // Define main variables
  40. var d3Container = d3.select(element);
  41. margin = { top: 20, right: 0, bottom: 30, left: 0 },
  42. width = d3Container.node().getBoundingClientRect().width - margin.left - margin.right,
  43. gridSize = width / new Date(data[data.length - 1].date).getHours(), // dynamically set grid size
  44. rowGap = 40, // vertical gap between rows
  45. height = (rowGap + gridSize) * (d3.max(nest, function(d,i) {return i+1})) - margin.top,
  46. buckets = 5, // number of colors in range
  47. colors = ['#DCEDC8','#C5E1A5','#9CCC65','#7CB342','#558B2F'];
  48. // Construct scales
  49. // ------------------------------
  50. // Horizontal
  51. var x = d3.time.scale().range([0, width]);
  52. // Vertical
  53. var y = d3.scale.linear().range([height, 0]);
  54. // Colors
  55. var colorScale = d3.scale.quantile()
  56. .domain([0, buckets - 1, d3.max(data, function (d) { return d.value; })])
  57. .range(colors);
  58. // Set input domains
  59. // ------------------------------
  60. // Horizontal
  61. x.domain([new Date(data[0].date), d3.time.hour.offset(new Date(data[data.length - 1].date), 1)]);
  62. // Vertical
  63. y.domain([0, d3.max(data, function(d) { return d.app; })]);
  64. // Create chart
  65. // ------------------------------
  66. // Container
  67. var container = d3Container.append('svg');
  68. // SVG element
  69. var svg = container
  70. .attr('width', width + margin.left + margin.right)
  71. .attr('height', height + margin.bottom)
  72. .append('g')
  73. .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  74. //
  75. // Append chart elements
  76. //
  77. // App groups
  78. // ------------------------------
  79. // Add groups for each app
  80. var hourGroup = svg.selectAll('.hour-group')
  81. .data(nest)
  82. .enter()
  83. .append('g')
  84. .attr('class', 'hour-group')
  85. .attr('transform', function(d, i) { return 'translate(0, ' + ((gridSize + rowGap) * i) +')'; });
  86. // Add app name
  87. hourGroup
  88. .append('text')
  89. .attr('class', 'd3-text')
  90. .attr('x', 0)
  91. .attr('y', -(margin.top/2))
  92. .text(function (d, i) { return d.key; });
  93. // Sales count text
  94. hourGroup
  95. .append('text')
  96. .attr('class', 'sales-count d3-text')
  97. .attr('x', width)
  98. .attr('y', -(margin.top/2))
  99. .style('text-anchor', 'end')
  100. .text(function (d, i) { return d3.sum(d.values, function(d) { return d.value; }) + ' sales today' });
  101. // Add map elements
  102. // ------------------------------
  103. // Add map squares
  104. var heatMap = hourGroup.selectAll('.heatmap-hour')
  105. .data(function(d) {return d.values})
  106. .enter()
  107. .append('rect')
  108. .attr('x', function(d,i) { return x(d.date); })
  109. .attr('y', 0)
  110. .attr('class', 'heatmap-hour d3-slice-border d3-bg')
  111. .attr('width', gridSize)
  112. .attr('height', gridSize)
  113. .style('cursor', 'pointer');
  114. // Add loading transition
  115. heatMap.transition()
  116. .duration(250)
  117. .delay(function(d, i) { return i * 20; })
  118. .style('fill', function(d) { return colorScale(d.value); })
  119. // Add user interaction
  120. hourGroup.each(function(d) {
  121. heatMap
  122. .on('mouseover', function (d, i) {
  123. d3.select(this).style('opacity', 0.75);
  124. d3.select(this.parentNode).select('.sales-count').text(function(d) { return d.values[i].value + ' sales at ' + formatTime(d.values[i].date); })
  125. })
  126. .on('mouseout', function (d, i) {
  127. d3.select(this).style('opacity', 1);
  128. d3.select(this.parentNode).select('.sales-count').text(function (d, i) { return d3.sum(d.values, function(d) { return d.value; }) + ' sales today' })
  129. })
  130. })
  131. // Add legend
  132. // ------------------------------
  133. // Get min and max values
  134. var minValue, maxValue;
  135. data.forEach(function(d, i) {
  136. maxValue = d3.max(data, function (d) { return d.value; });
  137. minValue = d3.min(data, function (d) { return d.value; });
  138. });
  139. // Place legend inside separate group
  140. var legendGroup = svg.append('g')
  141. .attr('class', 'legend-group')
  142. .attr('width', width)
  143. .attr('transform', 'translate(' + ((width/2) - ((buckets * gridSize))/2) + ',' + (height + (margin.bottom - margin.top)) + ')');
  144. // Then group legend elements
  145. var legend = legendGroup.selectAll('.heatmap-legend')
  146. .data([0].concat(colorScale.quantiles()), function(d) { return d; })
  147. .enter()
  148. .append('g')
  149. .attr('class', 'heatmap-legend');
  150. // Add legend items
  151. legend.append('rect')
  152. .attr('class', 'heatmap-legend-item d3-slice-border')
  153. .attr('x', function(d, i) { return gridSize * i; })
  154. .attr('y', -8)
  155. .attr('width', gridSize)
  156. .attr('height', 5)
  157. .style('stroke-width', 1)
  158. .style('fill', function(d, i) { return colors[i]; });
  159. // Add min value text label
  160. legendGroup.append('text')
  161. .attr('class', 'min-legend-value d3-text')
  162. .attr('x', -10)
  163. .attr('y', -2)
  164. .style('text-anchor', 'end')
  165. .style('font-size', 11)
  166. .text(minValue);
  167. // Add max value text label
  168. legendGroup.append('text')
  169. .attr('class', 'max-legend-value d3-text')
  170. .attr('x', (buckets * gridSize) + 10)
  171. .attr('y', -2)
  172. .style('font-size', 11)
  173. .text(maxValue);
  174. // Resize chart
  175. // ------------------------------
  176. // Call function on window resize
  177. window.addEventListener('resize', resizeHeatmap);
  178. // Call function on sidebar width change
  179. var sidebarToggle = document.querySelector('.sidebar-control');
  180. sidebarToggle && sidebarToggle.addEventListener('click', resizeHeatmap);
  181. // Resize function
  182. //
  183. // Since D3 doesn't support SVG resize by default,
  184. // we need to manually specify parts of the graph that need to
  185. // be updated on window resize
  186. function resizeHeatmap() {
  187. // Layout
  188. // -------------------------
  189. // Width
  190. width = d3Container.node().getBoundingClientRect().width - margin.left - margin.right,
  191. // Grid size
  192. gridSize = width / new Date(data[data.length - 1].date).getHours(),
  193. // Height
  194. height = (rowGap + gridSize) * (d3.max(nest, function(d,i) {return i+1})) - margin.top,
  195. // Main svg width
  196. container.attr('width', width + margin.left + margin.right).attr('height', height + margin.bottom);
  197. // Width of appended group
  198. svg.attr('width', width + margin.left + margin.right).attr('height', height + margin.bottom);
  199. // Horizontal range
  200. x.range([0, width]);
  201. // Chart elements
  202. // -------------------------
  203. // Groups for each app
  204. svg.selectAll('.hour-group')
  205. .attr('transform', function(d, i) { return 'translate(0, ' + ((gridSize + rowGap) * i) +')'; });
  206. // Map squares
  207. svg.selectAll('.heatmap-hour')
  208. .attr('width', gridSize)
  209. .attr('height', gridSize)
  210. .attr('x', function(d,i) { return x(d.date); });
  211. // Legend group
  212. svg.selectAll('.legend-group')
  213. .attr('transform', 'translate(' + ((width/2) - ((buckets * gridSize))/2) + ',' + (height + margin.bottom - margin.top) + ')');
  214. // Sales count text
  215. svg.selectAll('.sales-count')
  216. .attr('x', width);
  217. // Legend item
  218. svg.selectAll('.heatmap-legend-item')
  219. .attr('width', gridSize)
  220. .attr('x', function(d, i) { return gridSize * i; });
  221. // Max value text label
  222. svg.selectAll('.max-legend-value')
  223. .attr('x', (buckets * gridSize) + 10);
  224. }
  225. });
  226. }
  227. };
  228. //
  229. // Return objects assigned to module
  230. //
  231. return {
  232. init: function() {
  233. _AppSalesHeatmap('#sales-heatmap');
  234. }
  235. }
  236. }();
  237. // Initialize module
  238. // ------------------------------
  239. document.addEventListener('DOMContentLoaded', function() {
  240. DashboardHeatmaps.init();
  241. });