123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- /* ------------------------------------------------------------------------------
- *
- * # D3.js - horizontal bar chart
- *
- * Demo d3.js horizontal bar chart setup with .csv data source
- *
- * ---------------------------------------------------------------------------- */
- // Setup module
- // ------------------------------
- var DashboardHeatmaps = function() {
- //
- // Setup module components
- //
- // Daily sales heatmap
- var _AppSalesHeatmap = function(element) {
- if (typeof d3 == 'undefined') {
- console.warn('Warning - d3.min.js is not loaded.');
- return;
- }
- // Initialize chart only if element exsists in the DOM
- if($(element).length > 0) {
- // Load data
- d3.csv('../../../../global_assets/demo_data/dashboard/app_sales_heatmap.csv', function(error, data) {
- // Bind data
- // ------------------------------
- // Nest data
- var nested_data = d3.nest().key(function(d) { return d.app; }),
- nest = nested_data.entries(data);
- // Format date
- var format = d3.time.format('%Y/%m/%d %H:%M'),
- formatTime = d3.time.format('%H:%M');
- // Pull out values
- data.forEach(function(d, i) {
- d.date = format.parse(d.date),
- d.value = +d.value
- });
- // Layout setup
- // ------------------------------
- // Define main variables
- var d3Container = d3.select(element);
- margin = { top: 20, right: 0, bottom: 30, left: 0 },
- width = d3Container.node().getBoundingClientRect().width - margin.left - margin.right,
- gridSize = width / new Date(data[data.length - 1].date).getHours(), // dynamically set grid size
- rowGap = 40, // vertical gap between rows
- height = (rowGap + gridSize) * (d3.max(nest, function(d,i) {return i+1})) - margin.top,
- buckets = 5, // number of colors in range
- colors = ['#DCEDC8','#C5E1A5','#9CCC65','#7CB342','#558B2F'];
- // Construct scales
- // ------------------------------
- // Horizontal
- var x = d3.time.scale().range([0, width]);
- // Vertical
- var y = d3.scale.linear().range([height, 0]);
- // Colors
- var colorScale = d3.scale.quantile()
- .domain([0, buckets - 1, d3.max(data, function (d) { return d.value; })])
- .range(colors);
- // Set input domains
- // ------------------------------
- // Horizontal
- x.domain([new Date(data[0].date), d3.time.hour.offset(new Date(data[data.length - 1].date), 1)]);
- // Vertical
- y.domain([0, d3.max(data, function(d) { return d.app; })]);
- // Create chart
- // ------------------------------
- // Container
- var container = d3Container.append('svg');
- // SVG element
- var svg = container
- .attr('width', width + margin.left + margin.right)
- .attr('height', height + margin.bottom)
- .append('g')
- .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
- //
- // Append chart elements
- //
- // App groups
- // ------------------------------
- // Add groups for each app
- var hourGroup = svg.selectAll('.hour-group')
- .data(nest)
- .enter()
- .append('g')
- .attr('class', 'hour-group')
- .attr('transform', function(d, i) { return 'translate(0, ' + ((gridSize + rowGap) * i) +')'; });
- // Add app name
- hourGroup
- .append('text')
- .attr('class', 'd3-text')
- .attr('x', 0)
- .attr('y', -(margin.top/2))
- .text(function (d, i) { return d.key; });
- // Sales count text
- hourGroup
- .append('text')
- .attr('class', 'sales-count d3-text')
- .attr('x', width)
- .attr('y', -(margin.top/2))
- .style('text-anchor', 'end')
- .text(function (d, i) { return d3.sum(d.values, function(d) { return d.value; }) + ' sales today' });
- // Add map elements
- // ------------------------------
- // Add map squares
- var heatMap = hourGroup.selectAll('.heatmap-hour')
- .data(function(d) {return d.values})
- .enter()
- .append('rect')
- .attr('x', function(d,i) { return x(d.date); })
- .attr('y', 0)
- .attr('class', 'heatmap-hour d3-slice-border d3-bg')
- .attr('width', gridSize)
- .attr('height', gridSize)
- .style('cursor', 'pointer');
- // Add loading transition
- heatMap.transition()
- .duration(250)
- .delay(function(d, i) { return i * 20; })
- .style('fill', function(d) { return colorScale(d.value); })
- // Add user interaction
- hourGroup.each(function(d) {
- heatMap
- .on('mouseover', function (d, i) {
- d3.select(this).style('opacity', 0.75);
- d3.select(this.parentNode).select('.sales-count').text(function(d) { return d.values[i].value + ' sales at ' + formatTime(d.values[i].date); })
- })
- .on('mouseout', function (d, i) {
- d3.select(this).style('opacity', 1);
- d3.select(this.parentNode).select('.sales-count').text(function (d, i) { return d3.sum(d.values, function(d) { return d.value; }) + ' sales today' })
- })
- })
- // Add legend
- // ------------------------------
- // Get min and max values
- var minValue, maxValue;
- data.forEach(function(d, i) {
- maxValue = d3.max(data, function (d) { return d.value; });
- minValue = d3.min(data, function (d) { return d.value; });
- });
- // Place legend inside separate group
- var legendGroup = svg.append('g')
- .attr('class', 'legend-group')
- .attr('width', width)
- .attr('transform', 'translate(' + ((width/2) - ((buckets * gridSize))/2) + ',' + (height + (margin.bottom - margin.top)) + ')');
- // Then group legend elements
- var legend = legendGroup.selectAll('.heatmap-legend')
- .data([0].concat(colorScale.quantiles()), function(d) { return d; })
- .enter()
- .append('g')
- .attr('class', 'heatmap-legend');
- // Add legend items
- legend.append('rect')
- .attr('class', 'heatmap-legend-item d3-slice-border')
- .attr('x', function(d, i) { return gridSize * i; })
- .attr('y', -8)
- .attr('width', gridSize)
- .attr('height', 5)
- .style('stroke-width', 1)
- .style('fill', function(d, i) { return colors[i]; });
- // Add min value text label
- legendGroup.append('text')
- .attr('class', 'min-legend-value d3-text')
- .attr('x', -10)
- .attr('y', -2)
- .style('text-anchor', 'end')
- .style('font-size', 11)
- .text(minValue);
- // Add max value text label
- legendGroup.append('text')
- .attr('class', 'max-legend-value d3-text')
- .attr('x', (buckets * gridSize) + 10)
- .attr('y', -2)
- .style('font-size', 11)
- .text(maxValue);
- // Resize chart
- // ------------------------------
- // Call function on window resize
- window.addEventListener('resize', resizeHeatmap);
- // Call function on sidebar width change
- var sidebarToggle = document.querySelector('.sidebar-control');
- sidebarToggle && sidebarToggle.addEventListener('click', resizeHeatmap);
- // Resize function
- //
- // Since D3 doesn't support SVG resize by default,
- // we need to manually specify parts of the graph that need to
- // be updated on window resize
- function resizeHeatmap() {
- // Layout
- // -------------------------
- // Width
- width = d3Container.node().getBoundingClientRect().width - margin.left - margin.right,
- // Grid size
- gridSize = width / new Date(data[data.length - 1].date).getHours(),
- // Height
- height = (rowGap + gridSize) * (d3.max(nest, function(d,i) {return i+1})) - margin.top,
- // Main svg width
- container.attr('width', width + margin.left + margin.right).attr('height', height + margin.bottom);
- // Width of appended group
- svg.attr('width', width + margin.left + margin.right).attr('height', height + margin.bottom);
- // Horizontal range
- x.range([0, width]);
- // Chart elements
- // -------------------------
- // Groups for each app
- svg.selectAll('.hour-group')
- .attr('transform', function(d, i) { return 'translate(0, ' + ((gridSize + rowGap) * i) +')'; });
- // Map squares
- svg.selectAll('.heatmap-hour')
- .attr('width', gridSize)
- .attr('height', gridSize)
- .attr('x', function(d,i) { return x(d.date); });
- // Legend group
- svg.selectAll('.legend-group')
- .attr('transform', 'translate(' + ((width/2) - ((buckets * gridSize))/2) + ',' + (height + margin.bottom - margin.top) + ')');
- // Sales count text
- svg.selectAll('.sales-count')
- .attr('x', width);
- // Legend item
- svg.selectAll('.heatmap-legend-item')
- .attr('width', gridSize)
- .attr('x', function(d, i) { return gridSize * i; });
- // Max value text label
- svg.selectAll('.max-legend-value')
- .attr('x', (buckets * gridSize) + 10);
- }
- });
- }
- };
- //
- // Return objects assigned to module
- //
- return {
- init: function() {
- _AppSalesHeatmap('#sales-heatmap');
- }
- }
- }();
- // Initialize module
- // ------------------------------
- document.addEventListener('DOMContentLoaded', function() {
- DashboardHeatmaps.init();
- });
|